aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.gitmodules12
-rw-r--r--.idea/codeStyles/Project.xml81
-rw-r--r--.idea/copyright/Default.xml2
-rw-r--r--.idea/inspectionProfiles/Default.xml27
-rw-r--r--README.md27
-rw-r--r--app/build.gradle104
-rw-r--r--app/nonnull.gradle87
-rw-r--r--app/proguard-rules.pro3
-rw-r--r--app/src/debug/res/values/strings.xml4
-rw-r--r--app/src/main/java/com/wireguard/android/Application.java147
-rw-r--r--app/src/main/java/com/wireguard/android/BootShutdownReceiver.java38
-rw-r--r--app/src/main/java/com/wireguard/android/QuickTileService.java175
-rw-r--r--app/src/main/java/com/wireguard/android/activity/BaseActivity.java99
-rw-r--r--app/src/main/java/com/wireguard/android/activity/MainActivity.java139
-rw-r--r--app/src/main/java/com/wireguard/android/activity/SettingsActivity.java128
-rw-r--r--app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java82
-rw-r--r--app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java34
-rw-r--r--app/src/main/java/com/wireguard/android/backend/GoBackend.java241
-rw-r--r--app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java133
-rw-r--r--app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java103
-rw-r--r--app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java148
-rw-r--r--app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java140
-rw-r--r--app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java159
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java133
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/BaseFragment.java126
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java118
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java75
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java262
-rw-r--r--app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java479
-rw-r--r--app/src/main/java/com/wireguard/android/model/ApplicationData.java54
-rw-r--r--app/src/main/java/com/wireguard/android/model/Tunnel.java158
-rw-r--r--app/src/main/java/com/wireguard/android/model/TunnelManager.java300
-rw-r--r--app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java112
-rw-r--r--app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java91
-rw-r--r--app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java102
-rw-r--r--app/src/main/java/com/wireguard/android/preference/VersionPreference.java60
-rw-r--r--app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java119
-rw-r--r--app/src/main/java/com/wireguard/android/util/AsyncWorker.java63
-rw-r--r--app/src/main/java/com/wireguard/android/util/ClipboardUtils.java37
-rw-r--r--app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java96
-rw-r--r--app/src/main/java/com/wireguard/android/util/ErrorMessages.java130
-rw-r--r--app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java36
-rw-r--r--app/src/main/java/com/wireguard/android/util/FragmentUtils.java27
-rw-r--r--app/src/main/java/com/wireguard/android/util/ModuleLoader.java186
-rw-r--r--app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java109
-rw-r--r--app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java19
-rw-r--r--app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java198
-rw-r--r--app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java17
-rw-r--r--app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java93
-rw-r--r--app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java190
-rw-r--r--app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java380
-rw-r--r--app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java54
-rw-r--r--app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java59
-rw-r--r--app/src/main/java/com/wireguard/android/widget/NameInputFilter.java53
-rw-r--r--app/src/main/java/com/wireguard/android/widget/SlashDrawable.java216
-rw-r--r--app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java59
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java66
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java629
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java27
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java58
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java78
-rw-r--r--app/src/main/java/com/wireguard/config/InetAddresses.java64
-rw-r--r--app/src/main/java/com/wireguard/util/Keyed.java14
-rw-r--r--app/src/main/java/com/wireguard/util/KeyedList.java32
-rw-r--r--app/src/main/java/com/wireguard/util/SortedKeyedList.java31
-rw-r--r--app/src/main/res/drawable/fab_label_background.xml10
-rw-r--r--app/src/main/res/drawable/ic_action_edit_white.xml9
-rw-r--r--app/src/main/res/drawable/ic_action_open_white.xml9
-rw-r--r--app/src/main/res/layout-sw600dp/main_activity.xml25
-rw-r--r--app/src/main/res/layout/app_list_dialog_fragment.xml43
-rw-r--r--app/src/main/res/layout/tunnel_detail_fragment.xml138
-rw-r--r--app/src/main/res/layout/tunnel_detail_peer.xml94
-rw-r--r--app/src/main/res/layout/tunnel_editor_fragment.xml254
-rw-r--r--app/src/main/res/layout/tunnel_editor_peer.xml161
-rw-r--r--app/src/main/res/values-ldrtl/fab.xml4
-rw-r--r--app/src/main/res/values-night/colors.xml8
-rw-r--r--app/src/main/res/values/colors.xml8
-rw-r--r--app/src/main/res/values/dimens.xml4
-rw-r--r--app/src/main/res/values/fab.xml31
-rw-r--r--app/src/main/res/values/styles.xml13
-rw-r--r--app/src/main/res/xml/preferences.xml19
-rw-r--r--app/tools/CMakeLists.txt32
m---------app/tools/libmnl0
-rw-r--r--app/tools/libwg-go/Makefile35
-rw-r--r--app/tools/libwg-go/api-android.go176
-rw-r--r--app/tools/libwg-go/go.mod10
-rw-r--r--app/tools/libwg-go/go.sum19
-rw-r--r--app/tools/ndk-compat/compat.c77
-rw-r--r--app/tools/ndk-compat/compat.h16
m---------app/tools/wireguard0
-rw-r--r--build.gradle27
-rw-r--r--build.gradle.kts13
-rw-r--r--gradle.properties78
-rw-r--r--gradle/libs.versions.toml29
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin55616 -> 63721 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xgradlew283
-rw-r--r--settings.gradle1
-rw-r--r--settings.gradle.kts22
-rwxr-xr-xsync-crowdin.sh4
-rw-r--r--tunnel/build.gradle.kts128
-rw-r--r--tunnel/src/main/AndroidManifest.xml18
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Backend.java (renamed from app/src/main/java/com/wireguard/android/backend/Backend.java)46
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/BackendException.java61
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java439
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Statistics.java102
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java57
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java196
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/RootShell.java (renamed from app/src/main/java/com/wireguard/android/util/RootShell.java)81
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java (renamed from app/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java)45
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java (renamed from app/src/main/java/com/wireguard/android/util/ToolsInstaller.java)106
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Attribute.java (renamed from app/src/main/java/com/wireguard/config/Attribute.java)21
-rw-r--r--tunnel/src/main/java/com/wireguard/config/BadConfigException.java (renamed from app/src/main/java/com/wireguard/config/BadConfigException.java)10
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Config.java (renamed from app/src/main/java/com/wireguard/config/Config.java)16
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetAddresses.java86
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetEndpoint.java (renamed from app/src/main/java/com/wireguard/config/InetEndpoint.java)15
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetNetwork.java (renamed from app/src/main/java/com/wireguard/config/InetNetwork.java)7
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Interface.java (renamed from app/src/main/java/com/wireguard/config/Interface.java)94
-rw-r--r--tunnel/src/main/java/com/wireguard/config/ParseException.java (renamed from app/src/main/java/com/wireguard/config/ParseException.java)6
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Peer.java (renamed from app/src/main/java/com/wireguard/config/Peer.java)9
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/Curve25519.java (renamed from app/src/main/java/com/wireguard/crypto/Curve25519.java)9
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/Key.java (renamed from app/src/main/java/com/wireguard/crypto/Key.java)23
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java (renamed from app/src/main/java/com/wireguard/crypto/KeyFormatException.java)5
-rw-r--r--tunnel/src/main/java/com/wireguard/crypto/KeyPair.java (renamed from app/src/main/java/com/wireguard/crypto/KeyPair.java)5
-rw-r--r--tunnel/src/main/java/com/wireguard/util/NonNullForAll.java (renamed from app/src/main/java/com/wireguard/util/NonNullForAll.java)9
-rw-r--r--tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java173
-rw-r--r--tunnel/src/test/java/com/wireguard/config/ConfigTest.java49
-rw-r--r--tunnel/src/test/resources/broken.conf9
-rw-r--r--tunnel/src/test/resources/invalid-key.conf9
-rw-r--r--tunnel/src/test/resources/invalid-number.conf9
-rw-r--r--tunnel/src/test/resources/invalid-value.conf9
-rw-r--r--tunnel/src/test/resources/missing-attribute.conf8
-rw-r--r--tunnel/src/test/resources/missing-section.conf5
-rw-r--r--tunnel/src/test/resources/syntax-error.conf9
-rw-r--r--tunnel/src/test/resources/unknown-attribute.conf9
-rw-r--r--tunnel/src/test/resources/unknown-section.conf9
-rw-r--r--tunnel/src/test/resources/working.conf9
-rw-r--r--tunnel/tools/CMakeLists.txt44
m---------tunnel/tools/elf-cleaner0
-rw-r--r--tunnel/tools/libwg-go/.gitignore (renamed from app/tools/libwg-go/.gitignore)0
-rw-r--r--tunnel/tools/libwg-go/Makefile52
-rw-r--r--tunnel/tools/libwg-go/api-android.go227
-rw-r--r--tunnel/tools/libwg-go/go.mod14
-rw-r--r--tunnel/tools/libwg-go/go.sum13
-rw-r--r--tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff (renamed from app/tools/libwg-go/goruntime-boottime-over-monotonic.diff)104
-rw-r--r--tunnel/tools/libwg-go/jni.c (renamed from app/tools/libwg-go/jni.c)14
-rw-r--r--tunnel/tools/ndk-compat/compat.c25
-rw-r--r--tunnel/tools/ndk-compat/compat.h10
m---------tunnel/tools/wireguard-tools0
-rw-r--r--ui/build.gradle.kts93
-rw-r--r--ui/proguard-android-optimize.txt35
-rw-r--r--ui/sampledata/interface_names.json34
-rw-r--r--ui/src/debug/res/values/strings.xml4
-rw-r--r--ui/src/googleplay/AndroidManifest.xml8
-rw-r--r--ui/src/main/AndroidManifest.xml (renamed from app/src/main/AndroidManifest.xml)98
-rw-r--r--ui/src/main/java/com/wireguard/android/Application.kt157
-rw-r--r--ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt34
-rw-r--r--ui/src/main/java/com/wireguard/android/QuickTileService.kt187
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt96
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt382
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/MainActivity.kt129
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt107
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt29
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt66
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt445
-rw-r--r--ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt (renamed from app/src/main/java/com/wireguard/android/configStore/ConfigStore.java)39
-rw-r--r--ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt82
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt194
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt122
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/Keyed.kt12
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt32
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt106
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt82
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt104
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt170
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt114
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt82
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt150
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt333
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt342
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ApplicationData.kt22
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt146
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt61
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt255
-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.kt88
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt135
-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.kt79
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt63
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt113
-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.kt17
-rw-r--r--ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt80
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt37
-rw-r--r--ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt104
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt158
-rw-r--r--ui/src/main/java/com/wireguard/android/util/Extensions.kt31
-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.kt66
-rw-r--r--ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt152
-rw-r--r--ui/src/main/java/com/wireguard/android/util/UserKnobs.kt121
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt86
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt142
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt294
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt49
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt49
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt48
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt175
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt44
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/TvCardView.kt44
-rw-r--r--ui/src/main/res/anim/scale_down.xml12
-rw-r--r--ui/src/main/res/anim/scale_up.xml12
-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.xml (renamed from app/src/main/res/drawable/ic_action_add_white.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_action_delete.xml (renamed from app/src/main/res/drawable/ic_action_delete.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_action_edit.xml (renamed from app/src/main/res/drawable/ic_action_edit.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_action_generate.xml10
-rw-r--r--ui/src/main/res/drawable/ic_action_open.xml (renamed from app/src/main/res/drawable/ic_action_open.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_action_save.xml (renamed from app/src/main/res/drawable/ic_action_save.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_action_scan_qr_code.xml (renamed from app/src/main/res/drawable/ic_action_scan_qr_code_white.xml)9
-rw-r--r--ui/src/main/res/drawable/ic_action_select_all.xml (renamed from app/src/main/res/drawable/ic_action_select_all.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_action_share_white.xml10
-rw-r--r--ui/src/main/res/drawable/ic_arrow_back.xml15
-rw-r--r--ui/src/main/res/drawable/ic_launcher_foreground.xml (renamed from app/src/main/res/drawable/ic_launcher_foreground.xml)6
-rw-r--r--ui/src/main/res/drawable/ic_settings.xml (renamed from app/src/main/res/drawable/ic_settings.xml)7
-rw-r--r--ui/src/main/res/drawable/ic_tile.xml (renamed from app/src/main/res/drawable/ic_tile.xml)20
-rw-r--r--ui/src/main/res/drawable/list_item_background.xml (renamed from app/src/main/res/drawable/list_item_background.xml)6
-rw-r--r--ui/src/main/res/drawable/tv_logo_banner.xml211
-rw-r--r--ui/src/main/res/layout-sw600dp/main_activity.xml33
-rw-r--r--ui/src/main/res/layout/add_tunnels_bottom_sheet.xml78
-rw-r--r--ui/src/main/res/layout/app_list_dialog_fragment.xml67
-rw-r--r--ui/src/main/res/layout/app_list_item.xml (renamed from app/src/main/res/layout/app_list_item.xml)28
-rw-r--r--ui/src/main/res/layout/config_naming_dialog_fragment.xml (renamed from app/src/main/res/layout/config_naming_dialog_fragment.xml)9
-rw-r--r--ui/src/main/res/layout/log_viewer_activity.xml27
-rw-r--r--ui/src/main/res/layout/log_viewer_entry.xml32
-rw-r--r--ui/src/main/res/layout/main_activity.xml (renamed from app/src/main/res/layout/main_activity.xml)15
-rw-r--r--ui/src/main/res/layout/tunnel_detail_fragment.xml321
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml227
-rw-r--r--ui/src/main/res/layout/tunnel_editor_fragment.xml292
-rw-r--r--ui/src/main/res/layout/tunnel_editor_peer.xml200
-rw-r--r--ui/src/main/res/layout/tunnel_list_fragment.xml (renamed from app/src/main/res/layout/tunnel_list_fragment.xml)59
-rw-r--r--ui/src/main/res/layout/tunnel_list_item.xml (renamed from app/src/main/res/layout/tunnel_list_item.xml)26
-rw-r--r--ui/src/main/res/layout/tv_activity.xml154
-rw-r--r--ui/src/main/res/layout/tv_file_list_item.xml44
-rw-r--r--ui/src/main/res/layout/tv_tunnel_list_item.xml82
-rw-r--r--ui/src/main/res/menu/config_editor.xml (renamed from app/src/main/res/menu/config_editor.xml)0
-rw-r--r--ui/src/main/res/menu/log_viewer.xml9
-rw-r--r--ui/src/main/res/menu/main_activity.xml (renamed from app/src/main/res/menu/main_activity.xml)0
-rw-r--r--ui/src/main/res/menu/tunnel_detail.xml (renamed from app/src/main/res/menu/tunnel_detail.xml)0
-rw-r--r--ui/src/main/res/menu/tunnel_list_action_mode.xml (renamed from app/src/main/res/menu/tunnel_list_action_mode.xml)0
-rw-r--r--ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (renamed from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml)1
-rw-r--r--ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (renamed from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml)1
-rw-r--r--ui/src/main/res/mipmap-hdpi/ic_launcher.png (renamed from app/src/main/res/mipmap-hdpi/ic_launcher.png)bin6688 -> 6688 bytes
-rw-r--r--ui/src/main/res/mipmap-hdpi/ic_launcher_round.png (renamed from app/src/main/res/mipmap-hdpi/ic_launcher_round.png)bin7525 -> 7525 bytes
-rw-r--r--ui/src/main/res/mipmap-mdpi/ic_launcher.png (renamed from app/src/main/res/mipmap-mdpi/ic_launcher.png)bin3594 -> 3594 bytes
-rw-r--r--ui/src/main/res/mipmap-mdpi/ic_launcher_round.png (renamed from app/src/main/res/mipmap-mdpi/ic_launcher_round.png)bin4050 -> 4050 bytes
-rw-r--r--ui/src/main/res/mipmap-xhdpi/banner.pngbin0 -> 14988 bytes
-rw-r--r--ui/src/main/res/mipmap-xhdpi/ic_launcher.png (renamed from app/src/main/res/mipmap-xhdpi/ic_launcher.png)bin8904 -> 8904 bytes
-rw-r--r--ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png (renamed from app/src/main/res/mipmap-xhdpi/ic_launcher_round.png)bin10484 -> 10484 bytes
-rw-r--r--ui/src/main/res/mipmap-xxhdpi/ic_launcher.png (renamed from app/src/main/res/mipmap-xxhdpi/ic_launcher.png)bin16217 -> 16217 bytes
-rw-r--r--ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (renamed from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png)bin19531 -> 19531 bytes
-rw-r--r--ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png (renamed from app/src/main/res/mipmap-xxxhdpi/ic_launcher.png)bin22885 -> 22885 bytes
-rw-r--r--ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (renamed from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png)bin27146 -> 27146 bytes
-rw-r--r--ui/src/main/res/resources.properties1
-rw-r--r--ui/src/main/res/values-ar-rSA/strings.xml311
-rw-r--r--ui/src/main/res/values-ca-rES/strings.xml226
-rw-r--r--ui/src/main/res/values-cs-rCZ/strings.xml136
-rw-r--r--ui/src/main/res/values-da-rDK/strings.xml192
-rw-r--r--ui/src/main/res/values-de/strings.xml259
-rw-r--r--ui/src/main/res/values-el-rGR/strings.xml126
-rw-r--r--ui/src/main/res/values-es-rES/strings.xml245
-rw-r--r--ui/src/main/res/values-et-rEE/strings.xml257
-rw-r--r--ui/src/main/res/values-fa-rIR/strings.xml240
-rw-r--r--ui/src/main/res/values-fi-rFI/strings.xml90
-rw-r--r--ui/src/main/res/values-fr/strings.xml259
-rw-r--r--ui/src/main/res/values-hi-rIN/strings.xml219
-rw-r--r--ui/src/main/res/values-hi/strings.xml176
-rw-r--r--ui/src/main/res/values-hu-rHU/strings.xml36
-rw-r--r--ui/src/main/res/values-in/strings.xml225
-rw-r--r--ui/src/main/res/values-it/strings.xml259
-rw-r--r--ui/src/main/res/values-ja/strings.xml246
-rw-r--r--ui/src/main/res/values-ko-rKR/strings.xml223
-rw-r--r--ui/src/main/res/values-night/bools.xml5
-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.xml240
-rw-r--r--ui/src/main/res/values-no-rNO/strings.xml243
-rw-r--r--ui/src/main/res/values-pa-rIN/strings.xml233
-rw-r--r--ui/src/main/res/values-pl-rPL/strings.xml264
-rw-r--r--ui/src/main/res/values-pt-rBR/strings.xml226
-rw-r--r--ui/src/main/res/values-pt-rPT/strings.xml233
-rw-r--r--ui/src/main/res/values-ro-rRO/strings.xml259
-rw-r--r--ui/src/main/res/values-ru/strings.xml285
-rw-r--r--ui/src/main/res/values-si-rLK/strings.xml204
-rw-r--r--ui/src/main/res/values-sk-rSK/strings.xml158
-rw-r--r--ui/src/main/res/values-sl/strings.xml262
-rw-r--r--ui/src/main/res/values-sv-rSE/strings.xml259
-rw-r--r--ui/src/main/res/values-tr-rTR/strings.xml259
-rw-r--r--ui/src/main/res/values-uk-rUA/strings.xml270
-rw-r--r--ui/src/main/res/values-v23/styles.xml8
-rw-r--r--ui/src/main/res/values-v27/styles.xml10
-rw-r--r--ui/src/main/res/values-vi-rVN/strings.xml139
-rw-r--r--ui/src/main/res/values-zh-rCN/strings.xml246
-rw-r--r--ui/src/main/res/values-zh-rTW/strings.xml225
-rw-r--r--ui/src/main/res/values/attrs.xml (renamed from app/src/main/res/values/attrs.xml)4
-rw-r--r--ui/src/main/res/values/bools.xml5
-rw-r--r--ui/src/main/res/values/colors.xml63
-rw-r--r--ui/src/main/res/values/dimens.xml9
-rw-r--r--ui/src/main/res/values/ic_launcher_background.xml (renamed from app/src/main/res/values/ic_launcher_background.xml)0
-rw-r--r--ui/src/main/res/values/ids.xml (renamed from app/src/main/res/values/ids.xml)0
-rw-r--r--ui/src/main/res/values/logviewer_colors.xml6
-rw-r--r--ui/src/main/res/values/strings.xml (renamed from app/src/main/res/values/strings.xml)135
-rw-r--r--ui/src/main/res/values/styles.xml45
-rw-r--r--ui/src/main/res/values/themes.xml31
-rw-r--r--ui/src/main/res/xml/app_restrictions.xml13
-rw-r--r--ui/src/main/res/xml/preferences.xml45
320 files changed, 22474 insertions, 8995 deletions
diff --git a/.gitignore b/.gitignore
index a9e2ddae..0ab9d46e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,5 +14,5 @@ build/
*.dex
*.iml
*.jks
-keystore.properties
-package-info.java
+gradlew.bat
+maint/
diff --git a/.gitmodules b/.gitmodules
index 32ab0d51..e649c866 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
-[submodule "app/tools/libmnl"]
- path = app/tools/libmnl
- url = https://git.netfilter.org/libmnl/
-[submodule "app/tools/wireguard"]
- path = app/tools/wireguard
- url = https://git.zx2c4.com/WireGuard
+[submodule "tunnel/tools/wireguard-tools"]
+ path = tunnel/tools/wireguard-tools
+ url = https://git.zx2c4.com/wireguard-tools
+[submodule "tunnel/tools/elf-cleaner"]
+ path = tunnel/tools/elf-cleaner
+ url = https://github.com/termux/termux-elf-cleaner
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index b021530c..076a7f02 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -56,9 +56,16 @@
</value>
</option>
</JavaCodeStyleSettings>
- <XML>
- <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
- </XML>
+ <JetCodeStyleSettings>
+ <option name="PACKAGES_TO_USE_STAR_IMPORTS">
+ <value>
+ <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
+ </value>
+ </option>
+ <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
+ <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="METHOD_ANNOTATION_WRAP" value="0" />
<option name="FIELD_ANNOTATION_WRAP" value="0" />
@@ -358,6 +365,7 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -368,6 +376,7 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -379,6 +388,7 @@
<match>
<AND>
<NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -389,6 +399,7 @@
<match>
<AND>
<NAME>.*:name</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -399,6 +410,7 @@
<match>
<AND>
<NAME>name</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -409,6 +421,7 @@
<match>
<AND>
<NAME>style</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -419,6 +432,7 @@
<match>
<AND>
<NAME>.*</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -429,64 +443,12 @@
<rule>
<match>
<AND>
- <NAME>.*:layout_width</NAME>
- <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
- </AND>
- </match>
- </rule>
- </section>
- <section>
- <rule>
- <match>
- <AND>
- <NAME>.*:layout_height</NAME>
- <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
- </AND>
- </match>
- </rule>
- </section>
- <section>
- <rule>
- <match>
- <AND>
- <NAME>.*:layout_.*</NAME>
- <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
- </AND>
- </match>
- <order>BY_NAME</order>
- </rule>
- </section>
- <section>
- <rule>
- <match>
- <AND>
- <NAME>.*:width</NAME>
- <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
- </AND>
- </match>
- <order>BY_NAME</order>
- </rule>
- </section>
- <section>
- <rule>
- <match>
- <AND>
- <NAME>.*:height</NAME>
- <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
- </AND>
- </match>
- <order>BY_NAME</order>
- </rule>
- </section>
- <section>
- <rule>
- <match>
- <AND>
<NAME>.*</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
- <order>BY_NAME</order>
+ <order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
@@ -494,6 +456,7 @@
<match>
<AND>
<NAME>.*</NAME>
+ <XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
@@ -503,5 +466,9 @@
</rules>
</arrangement>
</codeStyleSettings>
+ <codeStyleSettings language="kotlin">
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ <option name="RIGHT_MARGIN" value="160" />
+ </codeStyleSettings>
</code_scheme>
</component> \ No newline at end of file
diff --git a/.idea/copyright/Default.xml b/.idea/copyright/Default.xml
index cb646393..db8533c5 100644
--- a/.idea/copyright/Default.xml
+++ b/.idea/copyright/Default.xml
@@ -1,6 +1,6 @@
<component name="CopyrightManager">
<copyright>
- <option name="notice" value="Copyright © &amp;#36;today.year WireGuard LLC. All Rights Reserved.&#10;SPDX-License-Identifier: Apache-2.0" />
+ <option name="notice" value="Copyright © 2017-&amp;#36;today.year WireGuard LLC. All Rights Reserved.&#10;SPDX-License-Identifier: Apache-2.0" />
<option name="myName" value="Default" />
</copyright>
</component>
diff --git a/.idea/inspectionProfiles/Default.xml b/.idea/inspectionProfiles/Default.xml
index f048a118..dd766357 100644
--- a/.idea/inspectionProfiles/Default.xml
+++ b/.idea/inspectionProfiles/Default.xml
@@ -9,7 +9,6 @@
</option>
<option name="nonThreadSafeTypes" value="" />
</inspection_tool>
- <inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -40,7 +39,9 @@
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
- <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
+ <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
+ </inspection_tool>
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
<option name="exceptionsString" value="" />
<option name="exceptions">
@@ -59,6 +60,9 @@
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
+ <inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="m_ignoreCatchBlocksWithComments" value="false" />
+ </inspection_tool>
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -103,9 +107,6 @@
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
- <inspection_tool class="DuplicateCondition" enabled="true" level="WARNING" enabled_by_default="true">
- <option name="ignoreSideEffectConditions" value="true" />
- </inspection_tool>
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
@@ -196,7 +197,6 @@
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
- <inspection_tool class="InnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="true" />
@@ -215,7 +215,6 @@
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
- <inspection_tool class="IntegerDivisionInFloatingPointContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
@@ -294,6 +293,7 @@
<option name="ignoreForLoopDeclarations" value="true" />
</inspection_tool>
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
+ <inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
@@ -347,7 +347,6 @@
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
- <inspection_tool class="PointlessNullCheck" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
@@ -394,7 +393,13 @@
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
- <inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true" />
+ <inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="allowedClasses">
+ <set>
+ <option value="org.junit.Assert" />
+ </set>
+ </option>
+ </inspection_tool>
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
@@ -432,7 +437,6 @@
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
- <option name="assertKeywordIsAssertion" value="false" />
</inspection_tool>
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
@@ -440,7 +444,6 @@
<option name="ignoreRethrownExceptions" value="true" />
</inspection_tool>
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
- <inspection_tool class="ToArrayCallWithZeroLengthArrayArgument" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_allowConstructorAsInitializer" value="false" />
<option name="m_onlyLookAtBlocks" value="true" />
@@ -466,10 +469,8 @@
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSwitchBranches" value="false" />
</inspection_tool>
- <inspection_tool class="UnnecessaryCallToStringValueOf" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
- <inspection_tool class="UnnecessaryDefault" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreJavadoc" value="false" />
diff --git a/README.md b/README.md
index 4db9a33e..eaffeee7 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,30 @@ $ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
$ cd wireguard-android
$ ./gradlew assembleRelease
```
+
+macOS users may need [flock(1)](https://github.com/discoteq/flock).
+
+## Embedding
+
+The tunnel library is [on Maven Central](https://search.maven.org/artifact/com.wireguard.android/tunnel), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
+
+```
+implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
+```
+
+The library makes use of Java 8 features, so be sure to support those in your gradle configuration with [desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring):
+
+```
+compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ coreLibraryDesugaringEnabled = true
+}
+dependencies {
+ coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
+}
+```
+
+## Translating
+
+Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 1c50f133..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,104 +0,0 @@
-apply plugin: 'com.android.application'
-apply from: 'nonnull.gradle'
-
-// Create a variable called keystorePropertiesFile, and initialize it to your
-// keystore.properties file, in the rootProject folder.
-final def keystorePropertiesFile = rootProject.file("keystore.properties")
-
-android {
- buildToolsVersion '29.0.2'
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- compileSdkVersion 29
- dataBinding.enabled true
- defaultConfig {
- applicationId 'com.wireguard.android'
- minSdkVersion 21
- targetSdkVersion 29
- versionCode 456
- versionName '0.0.20191016'
- buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
- }
- // If the keystore file exists
- if (keystorePropertiesFile.exists()) {
- // Initialize a new Properties() object called keystoreProperties.
- final def keystoreProperties = new Properties()
-
- // Load your keystore.properties file into the keystoreProperties object.
- keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
-
- signingConfigs {
- release {
- keyAlias keystoreProperties['keyAlias']
- keyPassword keystoreProperties['keyPassword']
- storeFile file(keystoreProperties['storeFile'])
- storePassword keystoreProperties['storePassword']
- }
- }
- }
- buildTypes {
- release {
- if (keystorePropertiesFile.exists()) signingConfig signingConfigs.release
- externalNativeBuild {
- cmake {
- arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
- }
- }
- minifyEnabled true
- proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
- }
- debug {
- applicationIdSuffix ".debug"
- versionNameSuffix "-debug"
- externalNativeBuild {
- cmake {
- arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}${applicationIdSuffix}"
- }
- }
- }
- }
- externalNativeBuild {
- cmake {
- path 'tools/CMakeLists.txt'
- }
- }
-}
-
-ext {
- annotationsVersion = '1.1.0'
- appcompatVersion = '1.1.0'
- cardviewVersion = '1.0.0'
- databindingVersion = '3.5.0'
- materialComponentsVersion = '1.0.0'
- jsr305Version = '3.0.2'
- preferenceVersion = '1.1.0'
- streamsupportVersion = '1.7.1'
- threetenabpVersion = '1.2.1'
- // ZXING switched minSdk to 24 so we cannot upgrade to 4.0.2 without following suit.
- // If you choose to upgrade to minSDK 24 then you should also disable Jetifier from
- // gradle.properties.
- zxingEmbeddedVersion = '3.6.0'
- eddsaVersion = '0.3.0'
-}
-
-dependencies {
- implementation "androidx.annotation:annotation:$annotationsVersion"
- implementation "androidx.appcompat:appcompat:$appcompatVersion"
- implementation "androidx.cardview:cardview:$cardviewVersion"
- implementation "androidx.databinding:databinding-runtime:$databindingVersion"
- implementation "androidx.preference:preference:$preferenceVersion"
- implementation "com.google.android.material:material:$materialComponentsVersion"
- implementation "com.google.code.findbugs:jsr305:$jsr305Version"
- implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
- implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
- implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
- implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
- implementation "net.i2p.crypto:eddsa:$eddsaVersion"
-}
-
-tasks.withType(JavaCompile) {
- options.compilerArgs << '-Xlint:unchecked'
- options.deprecation = true
-}
diff --git a/app/nonnull.gradle b/app/nonnull.gradle
deleted file mode 100644
index a10c10d2..00000000
--- a/app/nonnull.gradle
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-task generateNonNullJavaFiles(dependsOn: "assembleDebug", type: Copy) {
- group = "Copying"
- description = "Generate package-info.java classes"
-
- def basePackage = "com" + File.separatorChar + "wireguard"
- def mainSrcPhrase = "src" + File.separatorChar + "main" + File.separatorChar +
- "java" + File.separatorChar
- def mainTestSrcPhrase = "src" + File.separatorChar + "test" + File.separatorChar +
- "java" + File.separatorChar
- def mainAndroidTestSrcPhrase = "src" + File.separatorChar + "androidTest" + File.separatorChar +
- "java" + File.separatorChar
-
- def sourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
- "main" + File.separatorChar + "java" + File.separatorChar +
- basePackage )
- def testSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
- "test" + File.separatorChar + "java" + File.separatorChar +
- basePackage)
- def androidTestSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File
- .separatorChar +
- "androidTest" + File.separatorChar + "java" + File.separatorChar +
- basePackage )
-
- generateInfoFiles(sourceDir, mainSrcPhrase);
- sourceDir.eachDirRecurse { dir ->
- generateInfoFiles(dir, mainSrcPhrase)
- }
- if (file(testSourceDir).exists()) {
- generateInfoFiles(testSourceDir, mainTestSrcPhrase);
- testSourceDir.eachDirRecurse { dir ->
- generateInfoFiles(dir, mainTestSrcPhrase)
- }
- }
- if (file(androidTestSourceDir).exists()) {
- generateInfoFiles(androidTestSourceDir, mainAndroidTestSrcPhrase);
- androidTestSourceDir.eachDirRecurse { dir ->
- generateInfoFiles(dir, mainAndroidTestSrcPhrase)
- }
- }
- println "[SUCCESS] NonNull generator: package-info.java files checked"
-}
-
-private void generateInfoFiles(File dir, String mainSrcPhrase) {
- def infoFileContentHeader = getFileContentHeader();
- def infoFileContentFooter = getFileContentFooter();
- def infoFilePath = dir.getAbsolutePath() + File.separatorChar + "package-info.java"
-
- //file(infoFilePath).delete(); //do not use in production code
- if (!file(infoFilePath).exists()) {
- def infoFileContentPackage = getFileContentPackage(dir.getAbsolutePath(), mainSrcPhrase);
- new File(infoFilePath).write(infoFileContentHeader +
- infoFileContentPackage + infoFileContentFooter)
- println "[dir] " + infoFilePath + " created";
- }
-}
-
-def getFileContentPackage(String path, String mainSrcPhrase) {
- def mainSrcPhraseIndex = path.indexOf(mainSrcPhrase)
- def output = path.substring(mainSrcPhraseIndex)
-
- // Win hotfix
- if (System.properties['os.name'].toLowerCase().contains('windows')) {
- output = output.replace("\\", "/")
- mainSrcPhrase = mainSrcPhrase.replace("\\", "/")
- }
-
- return "package " + output.replaceAll(mainSrcPhrase, "").replaceAll(
- "/", ".") + ";\n"
-}
-
-def getFileContentHeader() {
- return "/**\n" +
- " * Make all method parameters @NonNull by default.\n" +
- " */\n" +
- "@NonNullForAll\n"
-}
-
-def getFileContentFooter() {
- return "\n" +
- "import com.wireguard.util.NonNullForAll;\n"
-}
-
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
deleted file mode 100644
index 4e7b3d96..00000000
--- a/app/proguard-rules.pro
+++ /dev/null
@@ -1,3 +0,0 @@
-# Squelch all warnings, they're harmless but ProGuard
-# escalates them as errors.
--dontwarn sun.misc.Unsafe
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
deleted file mode 100644
index 60e016ea..00000000
--- a/app/src/debug/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="app_name">WireGuard β</string>
-</resources>
diff --git a/app/src/main/java/com/wireguard/android/Application.java b/app/src/main/java/com/wireguard/android/Application.java
deleted file mode 100644
index 744986e8..00000000
--- a/app/src/main/java/com/wireguard/android/Application.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import androidx.preference.PreferenceManager;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatDelegate;
-
-import com.wireguard.android.backend.Backend;
-import com.wireguard.android.backend.GoBackend;
-import com.wireguard.android.backend.WgQuickBackend;
-import com.wireguard.android.configStore.FileConfigStore;
-import com.wireguard.android.model.TunnelManager;
-import com.wireguard.android.util.AsyncWorker;
-import com.wireguard.android.util.ModuleLoader;
-import com.wireguard.android.util.RootShell;
-import com.wireguard.android.util.ToolsInstaller;
-
-import java.io.File;
-import java.lang.ref.WeakReference;
-
-import java9.util.concurrent.CompletableFuture;
-
-public class Application extends android.app.Application {
- @SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
- private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
- @SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
- @Nullable private Backend backend;
- @SuppressWarnings("NullableProblems") private RootShell rootShell;
- @SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
- @SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
- @SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader;
- @SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
-
- public Application() {
- weakSelf = new WeakReference<>(this);
- }
-
- public static Application get() {
- return weakSelf.get();
- }
-
- public static AsyncWorker getAsyncWorker() {
- return get().asyncWorker;
- }
-
- public static Backend getBackend() {
- final Application app = get();
- synchronized (app.futureBackend) {
- if (app.backend == null) {
- Backend backend = null;
- boolean didStartRootShell = false;
- if (!app.moduleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
- try {
- app.rootShell.start();
- didStartRootShell = true;
- app.moduleLoader.loadModule();
- } catch (final Exception ignored) {
- }
- }
- if (app.moduleLoader.isModuleLoaded()) {
- try {
- if (!didStartRootShell)
- app.rootShell.start();
- backend = new WgQuickBackend(app.getApplicationContext());
- } catch (final Exception ignored) {
- }
- }
- if (backend == null)
- backend = new GoBackend(app.getApplicationContext());
- app.backend = backend;
- }
- return app.backend;
- }
- }
-
- public static CompletableFuture<Backend> getBackendAsync() {
- return get().futureBackend;
- }
-
- public static RootShell getRootShell() {
- return get().rootShell;
- }
-
- public static SharedPreferences getSharedPreferences() {
- return get().sharedPreferences;
- }
-
- public static ToolsInstaller getToolsInstaller() {
- return get().toolsInstaller;
- }
- public static ModuleLoader getModuleLoader() {
- return get().moduleLoader;
- }
-
- public static TunnelManager getTunnelManager() {
- return get().tunnelManager;
- }
-
- @Override
- protected void attachBaseContext(final Context context) {
- super.attachBaseContext(context);
-
- if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
- final Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_HOME);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- System.exit(0);
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
- rootShell = new RootShell(getApplicationContext());
- toolsInstaller = new ToolsInstaller(getApplicationContext());
- moduleLoader = new ModuleLoader(getApplicationContext());
-
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- AppCompatDelegate.setDefaultNightMode(
- sharedPreferences.getBoolean("dark_theme", false) ?
- AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
- } else {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
- }
-
- tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
- tunnelManager.onCreate();
-
- asyncWorker.supplyAsync(Application::getBackend).thenAccept(futureBackend::complete);
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/BootShutdownReceiver.java b/app/src/main/java/com/wireguard/android/BootShutdownReceiver.java
deleted file mode 100644
index e3ffce7a..00000000
--- a/app/src/main/java/com/wireguard/android/BootShutdownReceiver.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import com.wireguard.android.backend.WgQuickBackend;
-import com.wireguard.android.model.TunnelManager;
-import com.wireguard.android.util.ExceptionLoggers;
-
-public class BootShutdownReceiver extends BroadcastReceiver {
- private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
-
- @Override
- public void onReceive(final Context context, final Intent intent) {
- Application.getBackendAsync().thenAccept(backend -> {
- if (!(backend instanceof WgQuickBackend))
- return;
- final String action = intent.getAction();
- if (action == null)
- return;
- final TunnelManager tunnelManager = Application.getTunnelManager();
- if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
- Log.i(TAG, "Broadcast receiver restoring state (boot)");
- tunnelManager.restoreState(false).whenComplete(ExceptionLoggers.D);
- } else if (Intent.ACTION_SHUTDOWN.equals(action)) {
- Log.i(TAG, "Broadcast receiver saving state (shutdown)");
- tunnelManager.saveState();
- }
- });
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/QuickTileService.java b/app/src/main/java/com/wireguard/android/QuickTileService.java
deleted file mode 100644
index 425738ef..00000000
--- a/app/src/main/java/com/wireguard/android/QuickTileService.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android;
-
-import android.annotation.TargetApi;
-import android.content.Intent;
-import androidx.databinding.Observable;
-import androidx.databinding.Observable.OnPropertyChangedCallback;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.IBinder;
-import android.service.quicksettings.Tile;
-import android.service.quicksettings.TileService;
-import androidx.annotation.Nullable;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.wireguard.android.activity.MainActivity;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.model.Tunnel.State;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.widget.SlashDrawable;
-
-import java.util.Objects;
-
-/**
- * Service that maintains the application's custom Quick Settings tile. This service is bound by the
- * system framework as necessary to update the appearance of the tile in the system UI, and to
- * forward click events to the application.
- */
-
-@TargetApi(Build.VERSION_CODES.N)
-public class QuickTileService extends TileService {
- private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName();
-
- private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
- private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
- @Nullable private Icon iconOff;
- @Nullable private Icon iconOn;
- @Nullable private Tunnel tunnel;
-
- /* This works around an annoying unsolved frameworks bug some people are hitting. */
- @Override
- @Nullable
- public IBinder onBind(final Intent intent) {
- IBinder ret = null;
- try {
- ret = super.onBind(intent);
- } catch (final Exception e) {
- Log.d(TAG, "Failed to bind to TileService", e);
- }
- return ret;
- }
-
- @Override
- public void onClick() {
- if (tunnel != null) {
- final Tile tile = getQsTile();
- if (tile != null) {
- tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
- tile.updateTile();
- }
- tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
- } else {
- final Intent intent = new Intent(this, MainActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivityAndCollapse(intent);
- }
- }
-
- @Override
- public void onCreate() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- iconOff = iconOn = Icon.createWithResource(this, R.drawable.ic_tile);
- return;
- }
- final SlashDrawable icon = new SlashDrawable(getResources().getDrawable(R.drawable.ic_tile, Application.get().getTheme()));
- icon.setAnimationEnabled(false); /* Unfortunately we can't have animations, since Icons are marshaled. */
- icon.setSlashed(false);
- Bitmap b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
- Canvas c = new Canvas(b);
- icon.setBounds(0, 0, c.getWidth(), c.getHeight());
- icon.draw(c);
- iconOn = Icon.createWithBitmap(b);
- icon.setSlashed(true);
- b = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
- c = new Canvas(b);
- icon.setBounds(0, 0, c.getWidth(), c.getHeight());
- icon.draw(c);
- iconOff = Icon.createWithBitmap(b);
- }
-
- @Override
- public void onStartListening() {
- Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback);
- if (tunnel != null)
- tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
- updateTile();
- }
-
- @Override
- public void onStopListening() {
- if (tunnel != null)
- tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
- Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback);
- }
-
- private void onToggleFinished(@SuppressWarnings("unused") final State state,
- @Nullable final Throwable throwable) {
- if (throwable == null)
- return;
- final String error = ErrorMessages.get(throwable);
- final String message = getString(R.string.toggle_error, error);
- Log.e(TAG, message, throwable);
- Toast.makeText(this, message, Toast.LENGTH_LONG).show();
- }
-
- private void updateTile() {
- // Update the tunnel.
- final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
- if (newTunnel != tunnel) {
- if (tunnel != null)
- tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
- tunnel = newTunnel;
- if (tunnel != null)
- tunnel.addOnPropertyChangedCallback(onStateChangedCallback);
- }
- // Update the tile contents.
- final String label;
- final int state;
- final Tile tile = getQsTile();
- if (tunnel != null) {
- label = tunnel.getName();
- state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
- } else {
- label = getString(R.string.app_name);
- state = Tile.STATE_INACTIVE;
- }
- if (tile == null)
- return;
- tile.setLabel(label);
- if (tile.getState() != state) {
- tile.setIcon(state == Tile.STATE_ACTIVE ? iconOn : iconOff);
- tile.setState(state);
- }
- tile.updateTile();
- }
-
- private final class OnStateChangedCallback extends OnPropertyChangedCallback {
- @Override
- public void onPropertyChanged(final Observable sender, final int propertyId) {
- if (!Objects.equals(sender, tunnel)) {
- sender.removeOnPropertyChangedCallback(this);
- return;
- }
- if (propertyId != 0 && propertyId != BR.state)
- return;
- updateTile();
- }
- }
-
- private final class OnTunnelChangedCallback extends OnPropertyChangedCallback {
- @Override
- public void onPropertyChanged(final Observable sender, final int propertyId) {
- if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
- return;
- updateTile();
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/activity/BaseActivity.java b/app/src/main/java/com/wireguard/android/activity/BaseActivity.java
deleted file mode 100644
index b27ca6dd..00000000
--- a/app/src/main/java/com/wireguard/android/activity/BaseActivity.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.activity;
-
-import androidx.databinding.CallbackRegistry;
-import androidx.databinding.CallbackRegistry.NotifierCallback;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.model.Tunnel;
-
-import java.util.Objects;
-
-/**
- * Base class for activities that need to remember the currently-selected tunnel.
- */
-
-public abstract class BaseActivity extends ThemeChangeAwareActivity {
- private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
-
- private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
- @Nullable private Tunnel selectedTunnel;
-
- public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
- selectionChangeRegistry.add(listener);
- }
-
- @Nullable
- public Tunnel getSelectedTunnel() {
- return selectedTunnel;
- }
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- // Restore the saved tunnel if there is one; otherwise grab it from the arguments.
- final String savedTunnelName;
- if (savedInstanceState != null)
- savedTunnelName = savedInstanceState.getString(KEY_SELECTED_TUNNEL);
- else if (getIntent() != null)
- savedTunnelName = getIntent().getStringExtra(KEY_SELECTED_TUNNEL);
- else
- savedTunnelName = null;
-
- if (savedTunnelName != null)
- Application.getTunnelManager().getTunnels()
- .thenAccept(tunnels -> setSelectedTunnel(tunnels.get(savedTunnelName)));
-
- // The selected tunnel must be set before the superclass method recreates fragments.
- super.onCreate(savedInstanceState);
- }
-
- @Override
- protected void onSaveInstanceState(final Bundle outState) {
- if (selectedTunnel != null)
- outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel.getName());
- super.onSaveInstanceState(outState);
- }
-
- protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
-
- public void removeOnSelectedTunnelChangedListener(
- final OnSelectedTunnelChangedListener listener) {
- selectionChangeRegistry.remove(listener);
- }
-
- public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
- final Tunnel oldTunnel = selectedTunnel;
- if (Objects.equals(oldTunnel, tunnel))
- return;
- selectedTunnel = tunnel;
- onSelectedTunnelChanged(oldTunnel, tunnel);
- selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, tunnel);
- }
-
- public interface OnSelectedTunnelChangedListener {
- void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
- }
-
- private static final class SelectionChangeNotifier
- extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
- @Override
- public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
- final Tunnel oldTunnel, final int ignored,
- final Tunnel newTunnel) {
- listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
- }
- }
-
- private static final class SelectionChangeRegistry
- extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
- private SelectionChangeRegistry() {
- super(new SelectionChangeNotifier());
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/activity/MainActivity.java b/app/src/main/java/com/wireguard/android/activity/MainActivity.java
deleted file mode 100644
index ab9a4f8a..00000000
--- a/app/src/main/java/com/wireguard/android/activity/MainActivity.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.activity;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentTransaction;
-import androidx.appcompat.app.ActionBar;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import com.wireguard.android.R;
-import com.wireguard.android.fragment.TunnelDetailFragment;
-import com.wireguard.android.fragment.TunnelEditorFragment;
-import com.wireguard.android.fragment.TunnelListFragment;
-import com.wireguard.android.model.Tunnel;
-
-/**
- * CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
- * WireGuard application, and contains several fragments for listing, viewing details of, and
- * editing the configuration and interface state of WireGuard tunnels.
- */
-
-public class MainActivity extends BaseActivity
- implements FragmentManager.OnBackStackChangedListener {
- @Nullable private ActionBar actionBar;
- private boolean isTwoPaneLayout;
- @Nullable private TunnelListFragment listFragment;
-
- @Override
- public void onBackPressed() {
- final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
- // If the action menu is visible and expanded, collapse it instead of navigating back.
- if (isTwoPaneLayout || backStackEntries == 0) {
- if (listFragment != null && listFragment.collapseActionMenu())
- return;
- }
- // 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) {
- setSelectedTunnel(null);
- return;
- }
- super.onBackPressed();
- }
-
- @Override public void onBackStackChanged() {
- if (actionBar == null)
- return;
- // Do not show the home menu when the two-pane layout is at the detail view (see above).
- final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
- final int minBackStackEntries = isTwoPaneLayout ? 2 : 1;
- actionBar.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries);
- }
-
- // We use onTouchListener here to avoid the UI click sound, hence
- // calling View#performClick defeats the purpose of it.
- @SuppressLint("ClickableViewAccessibility")
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main_activity);
- actionBar = getSupportActionBar();
- isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
- listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
- getSupportFragmentManager().addOnBackStackChangedListener(this);
- onBackStackChanged();
- final View actionBarView = findViewById(R.id.action_bar);
- if (actionBarView != null)
- actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
- }
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- getMenuInflater().inflate(R.menu.main_activity, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // The back arrow in the action bar should act the same as the back button.
- onBackPressed();
- return true;
- case R.id.menu_action_edit:
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.detail_container, new TunnelEditorFragment())
- .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
- .addToBackStack(null)
- .commit();
- return true;
- case R.id.menu_action_save:
- // This menu item is handled by the editor fragment.
- return false;
- case R.id.menu_settings:
- startActivity(new Intent(this, SettingsActivity.class));
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- @Override
- protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
- @Nullable final Tunnel newTunnel) {
- final FragmentManager fragmentManager = getSupportFragmentManager();
- final int backStackEntries = fragmentManager.getBackStackEntryCount();
- if (newTunnel == null) {
- // Clear everything off the back stack (all editors and detail fragments).
- fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE);
- return;
- }
- if (backStackEntries == 2) {
- // Pop the editor off the back stack to reveal the detail fragment. Use the immediate
- // method to avoid the editor picking up the new tunnel while it is still visible.
- fragmentManager.popBackStackImmediate();
- } else if (backStackEntries == 0) {
- // Create and show a new detail fragment.
- fragmentManager.beginTransaction()
- .add(R.id.detail_container, new TunnelDetailFragment())
- .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
- .addToBackStack(null)
- .commit();
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java b/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java
deleted file mode 100644
index 442c93e6..00000000
--- a/app/src/main/java/com/wireguard/android/activity/SettingsActivity.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.activity;
-
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceFragmentCompat;
-import androidx.preference.PreferenceScreen;
-import android.util.SparseArray;
-import android.view.MenuItem;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.backend.WgQuickBackend;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Interface for changing application-global persistent settings.
- */
-
-public class SettingsActivity extends ThemeChangeAwareActivity {
- private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
- private int permissionRequestCounter;
-
- public void ensurePermissions(final String[] permissions, final PermissionRequestCallback cb) {
- final List<String> needPermissions = new ArrayList<>(permissions.length);
- for (final String permission : permissions) {
- if (ContextCompat.checkSelfPermission(this, permission)
- != PackageManager.PERMISSION_GRANTED)
- needPermissions.add(permission);
- }
- if (needPermissions.isEmpty()) {
- final int[] granted = new int[permissions.length];
- Arrays.fill(granted, PackageManager.PERMISSION_GRANTED);
- cb.done(permissions, granted);
- return;
- }
- final int idx = permissionRequestCounter++;
- permissionRequestCallbacks.put(idx, cb);
- ActivityCompat.requestPermissions(this,
- needPermissions.toArray(new String[needPermissions.size()]), idx);
- }
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
- getSupportFragmentManager().beginTransaction()
- .add(android.R.id.content, new SettingsFragment())
- .commit();
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- @Override
- public void onRequestPermissionsResult(final int requestCode,
- final String[] permissions,
- final int[] grantResults) {
- final PermissionRequestCallback f = permissionRequestCallbacks.get(requestCode);
- if (f != null) {
- permissionRequestCallbacks.remove(requestCode);
- f.done(permissions, grantResults);
- }
- }
-
- public interface PermissionRequestCallback {
- void done(String[] permissions, int[] grantResults);
- }
-
- public static class SettingsFragment extends PreferenceFragmentCompat {
- @Override
- public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
- addPreferencesFromResource(R.xml.preferences);
- final PreferenceScreen screen = getPreferenceScreen();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- screen.removePreference(getPreferenceManager().findPreference("dark_theme"));
-
- final Preference wgQuickOnlyPrefs[] = {
- getPreferenceManager().findPreference("tools_installer"),
- getPreferenceManager().findPreference("restore_on_boot")
- };
- for (final Preference pref : wgQuickOnlyPrefs)
- pref.setVisible(false);
- Application.getBackendAsync().thenAccept(backend -> {
- for (final Preference pref : wgQuickOnlyPrefs) {
- if (backend instanceof WgQuickBackend)
- pref.setVisible(true);
- else
- screen.removePreference(pref);
- }
- });
-
- final Preference moduleInstaller = getPreferenceManager().findPreference("module_downloader");
- moduleInstaller.setVisible(false);
- if (Application.getModuleLoader().isModuleLoaded()) {
- screen.removePreference(moduleInstaller);
- } else {
- Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> {
- if (e == null)
- moduleInstaller.setVisible(true);
- else
- screen.removePreference(moduleInstaller);
- });
- }
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java b/app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java
deleted file mode 100644
index 977f64ea..00000000
--- a/app/src/main/java/com/wireguard/android/activity/ThemeChangeAwareActivity.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.activity;
-
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.os.Build;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.app.AppCompatDelegate;
-import android.util.Log;
-
-import com.wireguard.android.Application;
-
-import java.lang.reflect.Field;
-
-public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
- private static boolean lastDarkMode;
- @Nullable private static Resources lastResources;
-
- private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
- if (resources == lastResources && darkMode == lastDarkMode)
- return;
-
- try {
- Field f;
- Object o = resources;
- try {
- f = o.getClass().getDeclaredField("mResourcesImpl");
- f.setAccessible(true);
- o = f.get(o);
- } catch (final Exception ignored) {
- }
- f = o.getClass().getDeclaredField("mDrawableCache");
- f.setAccessible(true);
- o = f.get(o);
- try {
- o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
- } catch (final Exception ignored) {
- o.getClass().getMethod("clear").invoke(o);
- }
- } catch (final Exception e) {
- Log.e(TAG, "Failed to flush drawable cache", e);
- }
-
- lastResources = resources;
- lastDarkMode = darkMode;
- }
-
-
- @Override
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
- Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
- }
-
- @Override
- protected void onDestroy() {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
- Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
- super.onDestroy();
- }
-
- @Override
- public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
- if ("dark_theme".equals(key)) {
- final boolean darkMode = sharedPreferences.getBoolean(key, false);
- AppCompatDelegate.setDefaultNightMode(
- sharedPreferences.getBoolean(key, false) ?
- AppCompatDelegate.MODE_NIGHT_YES :
- AppCompatDelegate.MODE_NIGHT_NO);
- invalidateDrawableCache(getResources(), darkMode);
- recreate();
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java b/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java
deleted file mode 100644
index 08ea8e7e..00000000
--- a/app/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.activity;
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-
-import com.wireguard.android.fragment.TunnelEditorFragment;
-import com.wireguard.android.model.Tunnel;
-
-/**
- * Standalone activity for creating tunnels.
- */
-
-public class TunnelCreatorActivity extends BaseActivity {
- @Override
- @SuppressWarnings("UnnecessaryFullyQualifiedName")
- protected void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
- getSupportFragmentManager().beginTransaction()
- .add(android.R.id.content, new TunnelEditorFragment())
- .commit();
- }
- }
-
- @Override
- protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
- finish();
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/backend/GoBackend.java b/app/src/main/java/com/wireguard/android/backend/GoBackend.java
deleted file mode 100644
index e85f2b0d..00000000
--- a/app/src/main/java/com/wireguard/android/backend/GoBackend.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.backend;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.ParcelFileDescriptor;
-import androidx.annotation.Nullable;
-import androidx.collection.ArraySet;
-import android.util.Log;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.activity.MainActivity;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.model.Tunnel.State;
-import com.wireguard.android.model.Tunnel.Statistics;
-import com.wireguard.android.util.ExceptionLoggers;
-import com.wireguard.android.util.SharedLibraryLoader;
-import com.wireguard.config.Config;
-import com.wireguard.config.InetNetwork;
-import com.wireguard.config.Peer;
-
-import java.net.InetAddress;
-import java.util.Collections;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import java9.util.concurrent.CompletableFuture;
-
-public final class GoBackend implements Backend {
- private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
- private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
-
- private final Context context;
- @Nullable private Tunnel currentTunnel;
- private int currentTunnelHandle = -1;
-
- public GoBackend(final Context context) {
- SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
- this.context = context;
- }
-
- private static native int wgGetSocketV4(int handle);
-
- private static native int wgGetSocketV6(int handle);
-
- private static native void wgTurnOff(int handle);
-
- private static native int wgTurnOn(String ifName, int tunFd, String settings);
-
- private static native String wgVersion();
-
- @Override
- public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
- if (tunnel.getState() == State.UP) {
- // Restart the tunnel to apply the new config.
- setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
- try {
- setStateInternal(tunnel, config, State.UP);
- } catch (final Exception e) {
- // The new configuration didn't work, so try to go back to the old one.
- setStateInternal(tunnel, tunnel.getConfig(), State.UP);
- throw e;
- }
- }
- return config;
- }
-
- @Override
- public Set<String> enumerate() {
- if (currentTunnel != null) {
- final Set<String> runningTunnels = new ArraySet<>();
- runningTunnels.add(currentTunnel.getName());
- return runningTunnels;
- }
- return Collections.emptySet();
- }
-
- @Override
- public State getState(final Tunnel tunnel) {
- return currentTunnel == tunnel ? State.UP : State.DOWN;
- }
-
- @Override
- public Statistics getStatistics(final Tunnel tunnel) {
- return new Statistics();
- }
-
- @Override
- public String getTypePrettyName() {
- return context.getString(R.string.type_name_go_userspace);
- }
-
- @Override
- public String getVersion() {
- return wgVersion();
- }
-
- @Override
- public State setState(final Tunnel tunnel, State state) throws Exception {
- final State originalState = getState(tunnel);
- if (state == State.TOGGLE)
- state = originalState == State.UP ? State.DOWN : State.UP;
- if (state == originalState)
- return originalState;
- if (state == State.UP && currentTunnel != null)
- throw new IllegalStateException(context.getString(R.string.multiple_tunnels_error));
- Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
- setStateInternal(tunnel, tunnel.getConfig(), state);
- return getState(tunnel);
- }
-
- private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
- throws Exception {
-
- if (state == State.UP) {
- Log.i(TAG, "Bringing tunnel up");
-
- Objects.requireNonNull(config, context.getString(R.string.no_config_error));
-
- if (VpnService.prepare(context) != null)
- throw new Exception(context.getString(R.string.vpn_not_authorized_error));
-
- final VpnService service;
- if (!vpnService.isDone())
- startVpnService();
-
- try {
- service = vpnService.get(2, TimeUnit.SECONDS);
- } catch (final TimeoutException e) {
- throw new Exception(context.getString(R.string.vpn_start_error), e);
- }
-
- if (currentTunnelHandle != -1) {
- Log.w(TAG, "Tunnel already up");
- return;
- }
-
- // Build config
- final String goConfig = config.toWgUserspaceString();
-
- // Create the vpn tunnel with android API
- final VpnService.Builder builder = service.getBuilder();
- builder.setSession(tunnel.getName());
-
- final Intent configureIntent = new Intent(context, MainActivity.class);
- configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
-
- for (final String excludedApplication : config.getInterface().getExcludedApplications())
- builder.addDisallowedApplication(excludedApplication);
-
- for (final InetNetwork addr : config.getInterface().getAddresses())
- builder.addAddress(addr.getAddress(), addr.getMask());
-
- for (final InetAddress addr : config.getInterface().getDnsServers())
- builder.addDnsServer(addr.getHostAddress());
-
- for (final Peer peer : config.getPeers()) {
- for (final InetNetwork addr : peer.getAllowedIps())
- builder.addRoute(addr.getAddress(), addr.getMask());
- }
-
- builder.setMtu(config.getInterface().getMtu().orElse(1280));
-
- builder.setBlocking(true);
- try (final ParcelFileDescriptor tun = builder.establish()) {
- if (tun == null)
- throw new Exception(context.getString(R.string.tun_create_error));
- Log.d(TAG, "Go backend v" + wgVersion());
- currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
- }
- if (currentTunnelHandle < 0)
- throw new Exception(context.getString(R.string.tunnel_on_error, currentTunnelHandle));
-
- currentTunnel = tunnel;
-
- service.protect(wgGetSocketV4(currentTunnelHandle));
- service.protect(wgGetSocketV6(currentTunnelHandle));
- } else {
- Log.i(TAG, "Bringing tunnel down");
-
- if (currentTunnelHandle == -1) {
- Log.w(TAG, "Tunnel already down");
- return;
- }
-
- wgTurnOff(currentTunnelHandle);
- currentTunnel = null;
- currentTunnelHandle = -1;
- }
- }
-
- private void startVpnService() {
- Log.d(TAG, "Requesting to start VpnService");
- context.startService(new Intent(context, VpnService.class));
- }
-
- public static class VpnService extends android.net.VpnService {
- public Builder getBuilder() {
- return new Builder();
- }
-
- @Override
- public void onCreate() {
- vpnService.complete(this);
- super.onCreate();
- }
-
- @Override
- public void onDestroy() {
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- for (final Tunnel tunnel : tunnels) {
- if (tunnel != null && tunnel.getState() != State.DOWN)
- tunnel.setState(State.DOWN);
- }
- });
-
- vpnService = vpnService.newIncompleteFuture();
- super.onDestroy();
- }
-
- @Override
- public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
- vpnService.complete(this);
- if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
- Log.d(TAG, "Service started by Always-on VPN feature");
- Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
- }
- return super.onStartCommand(intent, flags, startId);
- }
-
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
deleted file mode 100644
index 71427d8a..00000000
--- a/app/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.backend;
-
-import android.content.Context;
-import androidx.annotation.Nullable;
-import android.util.Log;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.model.Tunnel.State;
-import com.wireguard.android.model.Tunnel.Statistics;
-import com.wireguard.config.Config;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Set;
-
-import java9.util.stream.Collectors;
-import java9.util.stream.Stream;
-
-/**
- * WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
- */
-
-public final class WgQuickBackend implements Backend {
- private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
-
- private final File localTemporaryDir;
- private final Context context;
-
- public WgQuickBackend(final Context context) {
- localTemporaryDir = new File(context.getCacheDir(), "tmp");
- this.context = context;
- }
-
- @Override
- public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
- if (tunnel.getState() == State.UP) {
- // Restart the tunnel to apply the new config.
- setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
- try {
- setStateInternal(tunnel, config, State.UP);
- } catch (final Exception e) {
- // The new configuration didn't work, so try to go back to the old one.
- setStateInternal(tunnel, tunnel.getConfig(), State.UP);
- throw e;
- }
- }
- return config;
- }
-
- @Override
- public Set<String> enumerate() {
- final List<String> output = new ArrayList<>();
- // Don't throw an exception here or nothing will show up in the UI.
- try {
- Application.getToolsInstaller().ensureToolsAvailable();
- if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
- return Collections.emptySet();
- } catch (final Exception e) {
- Log.w(TAG, "Unable to enumerate running tunnels", e);
- return Collections.emptySet();
- }
- // wg puts all interface names on the same line. Split them into separate elements.
- return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
- }
-
- @Override
- public State getState(final Tunnel tunnel) {
- return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
- }
-
- @Override
- public Statistics getStatistics(final Tunnel tunnel) {
- return new Statistics();
- }
-
- @Override
- public String getTypePrettyName() {
- return context.getString(R.string.type_name_kernel_module);
- }
-
- @Override
- public String getVersion() throws Exception {
- final List<String> output = new ArrayList<>();
- if (Application.getRootShell()
- .run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
- throw new Exception(context.getString(R.string.module_version_error));
- return output.get(0);
- }
-
- @Override
- public State setState(final Tunnel tunnel, State state) throws Exception {
- final State originalState = getState(tunnel);
- if (state == State.TOGGLE)
- state = originalState == State.UP ? State.DOWN : State.UP;
- if (state == originalState)
- return originalState;
- Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
- Application.getToolsInstaller().ensureToolsAvailable();
- setStateInternal(tunnel, tunnel.getConfig(), state);
- return getState(tunnel);
- }
-
- private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
- Objects.requireNonNull(config, "Trying to set state with a null config");
-
- final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
- try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
- stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
- }
- String command = String.format("wg-quick %s '%s'",
- state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
- if (state == State.UP)
- command = "cat /sys/module/wireguard/version && " + command;
- final int result = Application.getRootShell().run(null, command);
- // noinspection ResultOfMethodCallIgnored
- tempFile.delete();
- if (result != 0)
- throw new Exception(context.getString(R.string.tunnel_config_error, result));
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java b/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java
deleted file mode 100644
index 45f2f759..00000000
--- a/app/src/main/java/com/wireguard/android/configStore/FileConfigStore.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.configStore;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.wireguard.android.R;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Config;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Set;
-
-import java9.util.stream.Collectors;
-import java9.util.stream.Stream;
-
-/**
- * Configuration store that uses a {@code wg-quick}-style file for each configured tunnel.
- */
-
-public final class FileConfigStore implements ConfigStore {
- private static final String TAG = "WireGuard/" + FileConfigStore.class.getSimpleName();
-
- private final Context context;
-
- public FileConfigStore(final Context context) {
- this.context = context;
- }
-
- @Override
- public Config create(final String name, final Config config) throws IOException {
- Log.d(TAG, "Creating configuration for tunnel " + name);
- final File file = fileFor(name);
- if (!file.createNewFile())
- throw new IOException(context.getString(R.string.config_file_exists_error, file.getName()));
- try (final FileOutputStream stream = new FileOutputStream(file, false)) {
- stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
- }
- return config;
- }
-
- @Override
- public void delete(final String name) throws IOException {
- Log.d(TAG, "Deleting configuration for tunnel " + name);
- final File file = fileFor(name);
- if (!file.delete())
- throw new IOException(context.getString(R.string.config_delete_error, file.getName()));
- }
-
- @Override
- public Set<String> enumerate() {
- return Stream.of(context.fileList())
- .filter(name -> name.endsWith(".conf"))
- .map(name -> name.substring(0, name.length() - ".conf".length()))
- .collect(Collectors.toUnmodifiableSet());
- }
-
- private File fileFor(final String name) {
- return new File(context.getFilesDir(), name + ".conf");
- }
-
- @Override
- public Config load(final String name) throws BadConfigException, IOException {
- try (final FileInputStream stream = new FileInputStream(fileFor(name))) {
- return Config.parse(stream);
- }
- }
-
- @Override
- public void rename(final String name, final String replacement) throws IOException {
- Log.d(TAG, "Renaming configuration for tunnel " + name + " to " + replacement);
- final File file = fileFor(name);
- final File replacementFile = fileFor(replacement);
- if (!replacementFile.createNewFile())
- throw new IOException(context.getString(R.string.config_exists_error, replacement));
- if (!file.renameTo(replacementFile)) {
- if (!replacementFile.delete())
- Log.w(TAG, "Couldn't delete marker file for new name " + replacement);
- throw new IOException(context.getString(R.string.config_rename_error, file.getName()));
- }
- }
-
- @Override
- public Config save(final String name, final Config config) throws IOException {
- Log.d(TAG, "Saving configuration for tunnel " + name);
- final File file = fileFor(name);
- if (!file.isFile())
- throw new FileNotFoundException(context.getString(R.string.config_not_found_error, file.getName()));
- try (final FileOutputStream stream = new FileOutputStream(file, false)) {
- stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
- }
- return config;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java b/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java
deleted file mode 100644
index ee216d4c..00000000
--- a/app/src/main/java/com/wireguard/android/databinding/BindingAdapters.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.databinding;
-
-import androidx.databinding.BindingAdapter;
-import androidx.databinding.DataBindingUtil;
-import androidx.databinding.ObservableList;
-import androidx.databinding.ViewDataBinding;
-import androidx.databinding.adapters.ListenerUtil;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.InputFilter;
-import android.view.LayoutInflater;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.wireguard.android.BR;
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler;
-import com.wireguard.android.util.ObservableKeyedList;
-import com.wireguard.android.widget.ToggleSwitch;
-import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
-import com.wireguard.config.Attribute;
-import com.wireguard.config.InetNetwork;
-import com.wireguard.util.Keyed;
-
-import java9.util.Optional;
-
-/**
- * Static methods for use by generated code in the Android data binding library.
- */
-
-@SuppressWarnings("unused")
-public final class BindingAdapters {
- private BindingAdapters() {
- // Prevent instantiation.
- }
-
- @BindingAdapter("checked")
- public static void setChecked(final ToggleSwitch view, final boolean checked) {
- view.setCheckedInternal(checked);
- }
-
- @BindingAdapter("filter")
- public static void setFilter(final TextView view, final InputFilter filter) {
- view.setFilters(new InputFilter[]{filter});
- }
-
- @BindingAdapter({"items", "layout"})
- public static <E>
- void setItems(final LinearLayout view,
- @Nullable final ObservableList<E> oldList, final int oldLayoutId,
- @Nullable final ObservableList<E> newList, final int newLayoutId) {
- if (oldList == newList && oldLayoutId == newLayoutId)
- return;
- ItemChangeListener<E> listener = ListenerUtil.getListener(view, R.id.item_change_listener);
- // If the layout changes, any existing listener must be replaced.
- if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
- listener.setList(null);
- listener = null;
- // Stop tracking the old listener.
- ListenerUtil.trackListener(view, null, R.id.item_change_listener);
- }
- // Avoid adding a listener when there is no new list or layout.
- if (newList == null || newLayoutId == 0)
- return;
- if (listener == null) {
- listener = new ItemChangeListener<>(view, newLayoutId);
- ListenerUtil.trackListener(view, listener, R.id.item_change_listener);
- }
- // Either the list changed, or this is an entirely new listener because the layout changed.
- listener.setList(newList);
- }
-
- @BindingAdapter({"items", "layout"})
- public static <E>
- void setItems(final LinearLayout view,
- @Nullable final Iterable<E> oldList, final int oldLayoutId,
- @Nullable final Iterable<E> newList, final int newLayoutId) {
- if (oldList == newList && oldLayoutId == newLayoutId)
- return;
- view.removeAllViews();
- if (newList == null)
- return;
- final LayoutInflater layoutInflater = LayoutInflater.from(view.getContext());
- for (final E item : newList) {
- final ViewDataBinding binding =
- DataBindingUtil.inflate(layoutInflater, newLayoutId, view, false);
- binding.setVariable(BR.collection, newList);
- binding.setVariable(BR.item, item);
- binding.executePendingBindings();
- view.addView(binding.getRoot());
- }
- }
-
- @BindingAdapter(requireAll = false, value = {"items", "layout", "configurationHandler"})
- public static <K, E extends Keyed<? extends K>>
- void setItems(final RecyclerView view,
- @Nullable final ObservableKeyedList<K, E> oldList, final int oldLayoutId,
- final RowConfigurationHandler oldRowConfigurationHandler,
- @Nullable final ObservableKeyedList<K, E> newList, final int newLayoutId,
- final RowConfigurationHandler newRowConfigurationHandler) {
- if (view.getLayoutManager() == null)
- view.setLayoutManager(new LinearLayoutManager(view.getContext(), RecyclerView.VERTICAL, false));
-
- if (oldList == newList && oldLayoutId == newLayoutId)
- return;
- // The ListAdapter interface is not generic, so this cannot be checked.
- @SuppressWarnings("unchecked") ObservableKeyedRecyclerViewAdapter<K, E> adapter =
- (ObservableKeyedRecyclerViewAdapter<K, E>) view.getAdapter();
- // If the layout changes, any existing adapter must be replaced.
- if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
- adapter.setList(null);
- adapter = null;
- }
- // Avoid setting an adapter when there is no new list or layout.
- if (newList == null || newLayoutId == 0)
- return;
- if (adapter == null) {
- adapter = new ObservableKeyedRecyclerViewAdapter<>(view.getContext(), newLayoutId, newList);
- view.setAdapter(adapter);
- }
-
- adapter.setRowConfigurationHandler(newRowConfigurationHandler);
- // Either the list changed, or this is an entirely new listener because the layout changed.
- adapter.setList(newList);
- }
-
- @BindingAdapter("onBeforeCheckedChanged")
- public static void setOnBeforeCheckedChanged(final ToggleSwitch view,
- final OnBeforeCheckedChangeListener listener) {
- view.setOnBeforeCheckedChangeListener(listener);
- }
-
- @BindingAdapter("android:text")
- public static void setText(final TextView view, final Optional<?> text) {
- view.setText(text.map(Object::toString).orElse(""));
- }
-
- @BindingAdapter("android:text")
- public static void setText(final TextView view, @Nullable final Iterable<InetNetwork> networks) {
- view.setText(networks != null ? Attribute.join(networks) : "");
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java b/app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java
deleted file mode 100644
index e7303eae..00000000
--- a/app/src/main/java/com/wireguard/android/databinding/ItemChangeListener.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.databinding;
-
-import androidx.databinding.DataBindingUtil;
-import androidx.databinding.ObservableList;
-import androidx.databinding.ViewDataBinding;
-import androidx.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.wireguard.android.BR;
-
-import java.lang.ref.WeakReference;
-import java.util.Objects;
-
-/**
- * Helper class for binding an ObservableList to the children of a ViewGroup.
- */
-
-class ItemChangeListener<T> {
- private final OnListChangedCallback<T> callback = new OnListChangedCallback<>(this);
- private final ViewGroup container;
- private final int layoutId;
- private final LayoutInflater layoutInflater;
- @Nullable private ObservableList<T> list;
-
- ItemChangeListener(final ViewGroup container, final int layoutId) {
- this.container = container;
- this.layoutId = layoutId;
- layoutInflater = LayoutInflater.from(container.getContext());
- }
-
- private View getView(final int position, @Nullable final View convertView) {
- ViewDataBinding binding = convertView != null ? DataBindingUtil.getBinding(convertView) : null;
- if (binding == null) {
- binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false);
- }
-
- Objects.requireNonNull(list, "Trying to get a view while list is still null");
-
- binding.setVariable(BR.collection, list);
- binding.setVariable(BR.item, list.get(position));
- binding.executePendingBindings();
- return binding.getRoot();
- }
-
- void setList(@Nullable final ObservableList<T> newList) {
- if (list != null)
- list.removeOnListChangedCallback(callback);
- list = newList;
- if (list != null) {
- list.addOnListChangedCallback(callback);
- callback.onChanged(list);
- } else {
- container.removeAllViews();
- }
- }
-
- private static final class OnListChangedCallback<T>
- extends ObservableList.OnListChangedCallback<ObservableList<T>> {
-
- private final WeakReference<ItemChangeListener<T>> weakListener;
-
- private OnListChangedCallback(final ItemChangeListener<T> listener) {
- weakListener = new WeakReference<>(listener);
- }
-
- @Override
- public void onChanged(final ObservableList<T> sender) {
- final ItemChangeListener<T> listener = weakListener.get();
- if (listener != null) {
- // TODO: recycle views
- listener.container.removeAllViews();
- for (int i = 0; i < sender.size(); ++i)
- listener.container.addView(listener.getView(i, null));
- } else {
- sender.removeOnListChangedCallback(this);
- }
- }
-
- @Override
- public void onItemRangeChanged(final ObservableList<T> sender, final int positionStart,
- final int itemCount) {
- final ItemChangeListener<T> listener = weakListener.get();
- if (listener != null) {
- for (int i = positionStart; i < positionStart + itemCount; ++i) {
- final View child = listener.container.getChildAt(i);
- listener.container.removeViewAt(i);
- listener.container.addView(listener.getView(i, child));
- }
- } else {
- sender.removeOnListChangedCallback(this);
- }
- }
-
- @Override
- public void onItemRangeInserted(final ObservableList<T> sender, final int positionStart,
- final int itemCount) {
- final ItemChangeListener<T> listener = weakListener.get();
- if (listener != null) {
- for (int i = positionStart; i < positionStart + itemCount; ++i)
- listener.container.addView(listener.getView(i, null));
- } else {
- sender.removeOnListChangedCallback(this);
- }
- }
-
- @Override
- public void onItemRangeMoved(final ObservableList<T> sender, final int fromPosition,
- final int toPosition, final int itemCount) {
- final ItemChangeListener<T> listener = weakListener.get();
- if (listener != null) {
- final View[] views = new View[itemCount];
- for (int i = 0; i < itemCount; ++i)
- views[i] = listener.container.getChildAt(fromPosition + i);
- listener.container.removeViews(fromPosition, itemCount);
- for (int i = 0; i < itemCount; ++i)
- listener.container.addView(views[i], toPosition + i);
- } else {
- sender.removeOnListChangedCallback(this);
- }
- }
-
- @Override
- public void onItemRangeRemoved(final ObservableList<T> sender, final int positionStart,
- final int itemCount) {
- final ItemChangeListener<T> listener = weakListener.get();
- if (listener != null) {
- listener.container.removeViews(positionStart, itemCount);
- } else {
- sender.removeOnListChangedCallback(this);
- }
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java b/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java
deleted file mode 100644
index 8b40dd91..00000000
--- a/app/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.databinding;
-
-import android.content.Context;
-import androidx.databinding.DataBindingUtil;
-import androidx.databinding.ObservableList;
-import androidx.databinding.ViewDataBinding;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-
-import com.wireguard.android.BR;
-import com.wireguard.android.util.ObservableKeyedList;
-import com.wireguard.util.Keyed;
-
-import java.lang.ref.WeakReference;
-
-/**
- * A generic {@code RecyclerView.Adapter} backed by a {@code ObservableKeyedList}.
- */
-
-public class ObservableKeyedRecyclerViewAdapter<K, E extends Keyed<? extends K>> extends Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder> {
-
- private final OnListChangedCallback<E> callback = new OnListChangedCallback<>(this);
- private final int layoutId;
- private final LayoutInflater layoutInflater;
- @Nullable private ObservableKeyedList<K, E> list;
- @Nullable private RowConfigurationHandler rowConfigurationHandler;
-
- ObservableKeyedRecyclerViewAdapter(final Context context, final int layoutId,
- final ObservableKeyedList<K, E> list) {
- this.layoutId = layoutId;
- layoutInflater = LayoutInflater.from(context);
- setList(list);
- }
-
- @Nullable
- private E getItem(final int position) {
- if (list == null || position < 0 || position >= list.size())
- return null;
- return list.get(position);
- }
-
- @Override
- public int getItemCount() {
- return list != null ? list.size() : 0;
- }
-
- @Override
- public long getItemId(final int position) {
- final K key = getKey(position);
- return key != null ? key.hashCode() : -1;
- }
-
- @Nullable
- private K getKey(final int position) {
- final E item = getItem(position);
- return item != null ? item.getKey() : null;
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public void onBindViewHolder(final ViewHolder holder, final int position) {
- holder.binding.setVariable(BR.collection, list);
- holder.binding.setVariable(BR.key, getKey(position));
- holder.binding.setVariable(BR.item, getItem(position));
- holder.binding.executePendingBindings();
-
- if (rowConfigurationHandler != null) {
- final E item = getItem(position);
- if (item != null) {
- rowConfigurationHandler.onConfigureRow(holder.binding, item, position);
- }
- }
- }
-
- @Override
- public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
- return new ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false));
- }
-
- void setList(@Nullable final ObservableKeyedList<K, E> newList) {
- if (list != null)
- list.removeOnListChangedCallback(callback);
- list = newList;
- if (list != null) {
- list.addOnListChangedCallback(callback);
- }
- notifyDataSetChanged();
- }
-
- void setRowConfigurationHandler(final RowConfigurationHandler rowConfigurationHandler) {
- this.rowConfigurationHandler = rowConfigurationHandler;
- }
-
- public interface RowConfigurationHandler<B extends ViewDataBinding, T> {
- void onConfigureRow(B binding, T item, int position);
- }
-
- private static final class OnListChangedCallback<E extends Keyed<?>>
- extends ObservableList.OnListChangedCallback<ObservableList<E>> {
-
- private final WeakReference<ObservableKeyedRecyclerViewAdapter<?, E>> weakAdapter;
-
- private OnListChangedCallback(final ObservableKeyedRecyclerViewAdapter<?, E> adapter) {
- weakAdapter = new WeakReference<>(adapter);
- }
-
- @Override
- public void onChanged(final ObservableList<E> sender) {
- final ObservableKeyedRecyclerViewAdapter adapter = weakAdapter.get();
- if (adapter != null)
- adapter.notifyDataSetChanged();
- else
- sender.removeOnListChangedCallback(this);
- }
-
- @Override
- public void onItemRangeChanged(final ObservableList<E> sender, final int positionStart,
- final int itemCount) {
- onChanged(sender);
- }
-
- @Override
- public void onItemRangeInserted(final ObservableList<E> sender, final int positionStart,
- final int itemCount) {
- onChanged(sender);
- }
-
- @Override
- public void onItemRangeMoved(final ObservableList<E> sender, final int fromPosition,
- final int toPosition, final int itemCount) {
- onChanged(sender);
- }
-
- @Override
- public void onItemRangeRemoved(final ObservableList<E> sender, final int positionStart,
- final int itemCount) {
- onChanged(sender);
- }
- }
-
- public static class ViewHolder extends RecyclerView.ViewHolder {
- final ViewDataBinding binding;
-
- public ViewHolder(final ViewDataBinding binding) {
- super(binding.getRoot());
-
- this.binding = binding;
- }
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java
deleted file mode 100644
index 67059c73..00000000
--- a/app/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-import androidx.appcompat.app.AlertDialog;
-import android.widget.Toast;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.AppListDialogFragmentBinding;
-import com.wireguard.android.model.ApplicationData;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.util.ObservableKeyedArrayList;
-import com.wireguard.android.util.ObservableKeyedList;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import java9.util.Comparators;
-
-public class AppListDialogFragment extends DialogFragment {
-
- private static final String KEY_EXCLUDED_APPS = "excludedApps";
- private final ObservableKeyedList<String, ApplicationData> appData = new ObservableKeyedArrayList<>();
- @Nullable private List<String> currentlyExcludedApps;
-
- public static <T extends Fragment & AppExclusionListener>
- AppListDialogFragment newInstance(final ArrayList<String> excludedApps, final T target) {
- final Bundle extras = new Bundle();
- extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps);
- final AppListDialogFragment fragment = new AppListDialogFragment();
- fragment.setTargetFragment(target, 0);
- fragment.setArguments(extras);
- return fragment;
- }
-
- private void loadData() {
- final Activity activity = getActivity();
- if (activity == null) {
- return;
- }
-
- final PackageManager pm = activity.getPackageManager();
- Application.getAsyncWorker().supplyAsync(() -> {
- final Intent launcherIntent = new Intent(Intent.ACTION_MAIN, null);
- launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(launcherIntent, 0);
-
- final List<ApplicationData> appData = new ArrayList<>();
- for (ResolveInfo resolveInfo : resolveInfos) {
- String packageName = resolveInfo.activityInfo.packageName;
- appData.add(new ApplicationData(resolveInfo.loadIcon(pm), resolveInfo.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)));
- }
-
- Collections.sort(appData, Comparators.comparing(ApplicationData::getName, String.CASE_INSENSITIVE_ORDER));
- return appData;
- }).whenComplete(((data, throwable) -> {
- if (data != null) {
- appData.clear();
- appData.addAll(data);
- } else {
- final String error = ErrorMessages.get(throwable);
- final String message = activity.getString(R.string.error_fetching_apps, error);
- Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
- dismissAllowingStateLoss();
- }
- }));
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- currentlyExcludedApps = getArguments().getStringArrayList(KEY_EXCLUDED_APPS);
- }
-
- @Override
- public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
- final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity());
- alertDialogBuilder.setTitle(R.string.excluded_applications);
-
- final AppListDialogFragmentBinding binding = AppListDialogFragmentBinding.inflate(getActivity().getLayoutInflater(), null, false);
- binding.executePendingBindings();
- alertDialogBuilder.setView(binding.getRoot());
-
- alertDialogBuilder.setPositiveButton(R.string.set_exclusions, (dialog, which) -> setExclusionsAndDismiss());
- alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
- alertDialogBuilder.setNeutralButton(R.string.deselect_all, (dialog, which) -> {
- });
-
- binding.setFragment(this);
- binding.setAppData(appData);
-
- loadData();
-
- final AlertDialog dialog = alertDialogBuilder.create();
- dialog.setOnShowListener(d -> dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(view -> {
- for (final ApplicationData app : appData)
- app.setExcludedFromTunnel(false);
- }));
- return dialog;
- }
-
- void setExclusionsAndDismiss() {
- final List<String> excludedApps = new ArrayList<>();
- for (final ApplicationData data : appData) {
- if (data.isExcludedFromTunnel()) {
- excludedApps.add(data.getPackageName());
- }
- }
-
- ((AppExclusionListener) getTargetFragment()).onExcludedAppsSelected(excludedApps);
- dismiss();
- }
-
- public interface AppExclusionListener {
- void onExcludedAppsSelected(List<String> excludedApps);
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java b/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java
deleted file mode 100644
index 00e26348..00000000
--- a/app/src/main/java/com/wireguard/android/fragment/BaseFragment.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.content.Context;
-import android.content.Intent;
-import androidx.databinding.DataBindingUtil;
-import androidx.databinding.ViewDataBinding;
-import androidx.annotation.Nullable;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.fragment.app.Fragment;
-import android.util.Log;
-import android.view.View;
-import android.widget.Toast;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.activity.BaseActivity;
-import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener;
-import com.wireguard.android.backend.GoBackend;
-import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
-import com.wireguard.android.databinding.TunnelListItemBinding;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.model.Tunnel.State;
-import com.wireguard.android.util.ErrorMessages;
-
-/**
- * Base class for fragments that need to know the currently-selected tunnel. Only does anything when
- * attached to a {@code BaseActivity}.
- */
-
-public abstract class BaseFragment extends Fragment implements OnSelectedTunnelChangedListener {
- private static final int REQUEST_CODE_VPN_PERMISSION = 23491;
- private static final String TAG = "WireGuard/" + BaseFragment.class.getSimpleName();
- @Nullable private BaseActivity activity;
- @Nullable private Tunnel pendingTunnel;
- @Nullable private Boolean pendingTunnelUp;
-
- @Nullable
- protected Tunnel getSelectedTunnel() {
- return activity != null ? activity.getSelectedTunnel() : null;
- }
-
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- if (requestCode == REQUEST_CODE_VPN_PERMISSION) {
- if (pendingTunnel != null && pendingTunnelUp != null)
- setTunnelStateWithPermissionsResult(pendingTunnel, pendingTunnelUp);
- pendingTunnel = null;
- pendingTunnelUp = null;
- }
- }
-
- @Override
- public void onAttach(final Context context) {
- super.onAttach(context);
- if (context instanceof BaseActivity) {
- activity = (BaseActivity) context;
- activity.addOnSelectedTunnelChangedListener(this);
- } else {
- activity = null;
- }
- }
-
- @Override
- public void onDetach() {
- if (activity != null)
- activity.removeOnSelectedTunnelChangedListener(this);
- activity = null;
- super.onDetach();
- }
-
- protected void setSelectedTunnel(@Nullable final Tunnel tunnel) {
- if (activity != null)
- activity.setSelectedTunnel(tunnel);
- }
-
- public void setTunnelState(final View view, final boolean checked) {
- final ViewDataBinding binding = DataBindingUtil.findBinding(view);
- final Tunnel tunnel;
- if (binding instanceof TunnelDetailFragmentBinding)
- tunnel = ((TunnelDetailFragmentBinding) binding).getTunnel();
- else if (binding instanceof TunnelListItemBinding)
- tunnel = ((TunnelListItemBinding) binding).getItem();
- else
- return;
- if (tunnel == null)
- return;
-
- Application.getBackendAsync().thenAccept(backend -> {
- if (backend instanceof GoBackend) {
- final Intent intent = GoBackend.VpnService.prepare(view.getContext());
- if (intent != null) {
- pendingTunnel = tunnel;
- pendingTunnelUp = checked;
- startActivityForResult(intent, REQUEST_CODE_VPN_PERMISSION);
- return;
- }
- }
-
- setTunnelStateWithPermissionsResult(tunnel, checked);
- });
- }
-
- private void setTunnelStateWithPermissionsResult(final Tunnel tunnel, final boolean checked) {
- tunnel.setState(State.of(checked)).whenComplete((state, throwable) -> {
- if (throwable == null)
- return;
- final String error = ErrorMessages.get(throwable);
- final int messageResId = checked ? R.string.error_up : R.string.error_down;
- final String message = getContext().getString(messageResId, error);
- final View view = getView();
- if (view != null)
- Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
- else
- Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
- Log.e(TAG, message, throwable);
- });
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java b/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java
deleted file mode 100644
index e8f0419c..00000000
--- a/app/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
-import androidx.appcompat.app.AlertDialog;
-import android.view.inputmethod.InputMethodManager;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Config;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-public class ConfigNamingDialogFragment extends DialogFragment {
- private static final String KEY_CONFIG_TEXT = "config_text";
-
- @Nullable private ConfigNamingDialogFragmentBinding binding;
- @Nullable private Config config;
- @Nullable private InputMethodManager imm;
-
- public static ConfigNamingDialogFragment newInstance(final String configText) {
- final Bundle extras = new Bundle();
- extras.putString(KEY_CONFIG_TEXT, configText);
- final ConfigNamingDialogFragment fragment = new ConfigNamingDialogFragment();
- fragment.setArguments(extras);
- return fragment;
- }
-
- private void createTunnelAndDismiss() {
- if (binding != null) {
- final String name = binding.tunnelNameText.getText().toString();
-
- Application.getTunnelManager().create(name, config).whenComplete((tunnel, throwable) -> {
- if (tunnel != null) {
- dismiss();
- } else {
- binding.tunnelNameTextLayout.setError(throwable.getMessage());
- }
- });
- }
- }
-
- @Override
- public void dismiss() {
- setKeyboardVisible(false);
- super.dismiss();
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final Bundle arguments = getArguments();
- final String configText = arguments.getString(KEY_CONFIG_TEXT);
- final byte[] configBytes = configText.getBytes(StandardCharsets.UTF_8);
- try {
- config = Config.parse(new ByteArrayInputStream(configBytes));
- } catch (final BadConfigException | IOException e) {
- throw new IllegalArgumentException("Invalid config passed to " + getClass().getSimpleName(), e);
- }
- }
-
- @Override
- public Dialog onCreateDialog(final Bundle savedInstanceState) {
- final Activity activity = Objects.requireNonNull(getActivity());
-
- imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
-
- final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
- alertDialogBuilder.setTitle(R.string.import_from_qr_code);
-
- binding = ConfigNamingDialogFragmentBinding.inflate(activity.getLayoutInflater(), null, false);
- binding.executePendingBindings();
- alertDialogBuilder.setView(binding.getRoot());
-
- alertDialogBuilder.setPositiveButton(R.string.create_tunnel, null);
- alertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, which) -> dismiss());
-
- return alertDialogBuilder.create();
- }
-
- @Override public void onResume() {
- super.onResume();
-
- final AlertDialog dialog = (AlertDialog) getDialog();
- if (dialog != null) {
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> createTunnelAndDismiss());
-
- setKeyboardVisible(true);
- }
- }
-
- private void setKeyboardVisible(final boolean visible) {
- Objects.requireNonNull(imm);
-
- if (visible) {
- imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
- } else if (binding != null) {
- imm.hideSoftInputFromWindow(binding.tunnelNameText.getWindowToken(), 0);
- }
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java
deleted file mode 100644
index 8d2be476..00000000
--- a/app/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
-import com.wireguard.android.model.Tunnel;
-
-/**
- * Fragment that shows details about a specific tunnel.
- */
-
-public class TunnelDetailFragment extends BaseFragment {
- @Nullable private TunnelDetailFragmentBinding binding;
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- inflater.inflate(R.menu.tunnel_detail, menu);
- }
-
- @Override
- public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
- binding.executePendingBindings();
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
- if (binding == null)
- return;
- binding.setTunnel(newTunnel);
- if (newTunnel == null)
- binding.setConfig(null);
- else
- newTunnel.getConfigAsync().thenAccept(binding::setConfig);
- }
-
- @Override
- public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
- if (binding == null) {
- return;
- }
-
- binding.setFragment(this);
- onSelectedTunnelChanged(null, getSelectedTunnel());
- super.onViewStateRestored(savedInstanceState);
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java
deleted file mode 100644
index 4990e69f..00000000
--- a/app/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.app.Activity;
-import android.content.Context;
-import androidx.databinding.ObservableList;
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.fragment.app.FragmentManager;
-import android.util.Log;
-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 android.view.inputmethod.InputMethodManager;
-import android.widget.Toast;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.databinding.TunnelEditorFragmentBinding;
-import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.model.TunnelManager;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.viewmodel.ConfigProxy;
-import com.wireguard.config.Config;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Fragment for editing a WireGuard configuration.
- */
-
-public class TunnelEditorFragment extends BaseFragment implements AppExclusionListener {
- private static final String KEY_LOCAL_CONFIG = "local_config";
- private static final String KEY_ORIGINAL_NAME = "original_name";
- private static final String TAG = "WireGuard/" + TunnelEditorFragment.class.getSimpleName();
-
- @Nullable private TunnelEditorFragmentBinding binding;
- @Nullable private Tunnel tunnel;
-
- private void onConfigLoaded(final Config config) {
- if (binding != null) {
- binding.setConfig(new ConfigProxy(config));
- }
- }
-
- private void onConfigSaved(final Tunnel savedTunnel,
- @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- message = getString(R.string.config_save_success, savedTunnel.getName());
- Log.d(TAG, message);
- Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
- onFinished();
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.config_save_error, savedTunnel.getName(), error);
- Log.e(TAG, message, throwable);
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
- }
-
- @Override
- public void onCreate(@Nullable final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- inflater.inflate(R.menu.config_editor, menu);
- }
-
- @Override
- public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- binding = TunnelEditorFragmentBinding.inflate(inflater, container, false);
- binding.executePendingBindings();
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onExcludedAppsSelected(final List<String> excludedApps) {
- Objects.requireNonNull(binding, "Tried to set excluded apps while no view was loaded");
- final ObservableList<String> excludedApplications =
- binding.getConfig().getInterface().getExcludedApplications();
- excludedApplications.clear();
- excludedApplications.addAll(excludedApps);
- }
-
- private void onFinished() {
- // Hide the keyboard; it rarely goes away on its own.
- final Activity activity = getActivity();
- if (activity == null) return;
- final View focusedView = activity.getCurrentFocus();
- if (focusedView != null) {
- final Object service = activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- final InputMethodManager inputManager = (InputMethodManager) service;
- if (inputManager != null)
- inputManager.hideSoftInputFromWindow(focusedView.getWindowToken(),
- InputMethodManager.HIDE_NOT_ALWAYS);
- }
- // Tell the activity to finish itself or go back to the detail view.
- getActivity().runOnUiThread(() -> {
- // TODO(smaeul): Remove this hack when fixing the Config ViewModel
- // The selected tunnel has to actually change, but we have to remember this one.
- final Tunnel savedTunnel = tunnel;
- if (savedTunnel == getSelectedTunnel())
- setSelectedTunnel(null);
- setSelectedTunnel(savedTunnel);
- });
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_save:
- if (binding == null)
- return false;
- final Config newConfig;
- try {
- newConfig = binding.getConfig().resolve();
- } catch (final Exception e) {
- final String error = ErrorMessages.get(e);
- final String tunnelName = tunnel == null ? binding.getName() : tunnel.getName();
- final String message = getString(R.string.config_save_error, tunnelName, error);
- Log.e(TAG, message, e);
- Snackbar.make(binding.mainContainer, error, Snackbar.LENGTH_LONG).show();
- return false;
- }
- if (tunnel == null) {
- Log.d(TAG, "Attempting to create new tunnel " + binding.getName());
- final TunnelManager manager = Application.getTunnelManager();
- manager.create(binding.getName(), newConfig)
- .whenComplete(this::onTunnelCreated);
- } else if (!tunnel.getName().equals(binding.getName())) {
- Log.d(TAG, "Attempting to rename tunnel to " + binding.getName());
- tunnel.setName(binding.getName())
- .whenComplete((a, b) -> onTunnelRenamed(tunnel, newConfig, b));
- } else {
- Log.d(TAG, "Attempting to save config of " + tunnel.getName());
- tunnel.setConfig(newConfig)
- .whenComplete((a, b) -> onConfigSaved(tunnel, b));
- }
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- public void onRequestSetExcludedApplications(@SuppressWarnings("unused") final View view) {
- final FragmentManager fragmentManager = getFragmentManager();
- if (fragmentManager != null && binding != null) {
- final ArrayList<String> excludedApps = new ArrayList<>(binding.getConfig().getInterface().getExcludedApplications());
- final AppListDialogFragment fragment = AppListDialogFragment.newInstance(excludedApps, this);
- fragment.show(fragmentManager, null);
- }
- }
-
- @Override
- public void onSaveInstanceState(final Bundle outState) {
- if (binding != null)
- outState.putParcelable(KEY_LOCAL_CONFIG, binding.getConfig());
- outState.putString(KEY_ORIGINAL_NAME, tunnel == null ? null : tunnel.getName());
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
- @Nullable final Tunnel newTunnel) {
- tunnel = newTunnel;
- if (binding == null)
- return;
- binding.setConfig(new ConfigProxy());
- if (tunnel != null) {
- binding.setName(tunnel.getName());
- tunnel.getConfigAsync().thenAccept(this::onConfigLoaded);
- } else {
- binding.setName("");
- }
- }
-
- private void onTunnelCreated(final Tunnel newTunnel, @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- tunnel = newTunnel;
- message = getString(R.string.tunnel_create_success, tunnel.getName());
- Log.d(TAG, message);
- Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
- onFinished();
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.tunnel_create_error, error);
- Log.e(TAG, message, throwable);
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
- }
-
- private void onTunnelRenamed(final Tunnel renamedTunnel, final Config newConfig,
- @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- message = getString(R.string.tunnel_rename_success, renamedTunnel.getName());
- Log.d(TAG, message);
- // Now save the rest of configuration changes.
- Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel.getName());
- renamedTunnel.setConfig(newConfig).whenComplete((a, b) -> onConfigSaved(renamedTunnel, b));
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.tunnel_rename_error, error);
- Log.e(TAG, message, throwable);
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
- }
-
- @Override
- public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
- if (binding == null) {
- return;
- }
-
- binding.setFragment(this);
-
- if (savedInstanceState == null) {
- onSelectedTunnelChanged(null, getSelectedTunnel());
- } else {
- tunnel = getSelectedTunnel();
- final ConfigProxy config = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG);
- final String originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME);
- if (tunnel != null && !tunnel.getName().equals(originalName))
- onSelectedTunnelChanged(null, tunnel);
- else
- binding.setConfig(config);
- }
-
- super.onViewStateRestored(savedInstanceState);
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java b/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
deleted file mode 100644
index 59260500..00000000
--- a/app/src/main/java/com/wireguard/android/fragment/TunnelListFragment.java
+++ /dev/null
@@ -1,479 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.fragment;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.OpenableColumns;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.fragment.app.FragmentManager;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.view.ActionMode;
-import androidx.recyclerview.widget.RecyclerView;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.google.zxing.integration.android.IntentIntegrator;
-import com.google.zxing.integration.android.IntentResult;
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.activity.TunnelCreatorActivity;
-import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter;
-import com.wireguard.android.databinding.TunnelListFragmentBinding;
-import com.wireguard.android.databinding.TunnelListItemBinding;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.widget.MultiselectableRelativeLayout;
-import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Config;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-import java9.util.concurrent.CompletableFuture;
-import java9.util.stream.StreamSupport;
-
-/**
- * Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
- */
-
-public class TunnelListFragment extends BaseFragment {
- private static final int REQUEST_IMPORT = 1;
- private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
-
- private final ActionModeListener actionModeListener = new ActionModeListener();
- @Nullable private ActionMode actionMode;
- @Nullable private TunnelListFragmentBinding binding;
-
- public boolean collapseActionMenu() {
- if (binding != null && binding.createMenu.isExpanded()) {
- binding.createMenu.collapse();
- return true;
- }
- return false;
- }
-
- private void importTunnel(@NonNull final String configText) {
- try {
- // Ensure the config text is parseable before proceeding…
- Config.parse(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8)));
-
- // Config text is valid, now create the tunnel…
- final FragmentManager fragmentManager = getFragmentManager();
- if (fragmentManager != null)
- ConfigNamingDialogFragment.newInstance(configText).show(fragmentManager, null);
- } catch (final BadConfigException | IOException e) {
- onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(e));
- }
- }
-
- private void importTunnel(@Nullable final Uri uri) {
- final Activity activity = getActivity();
- if (activity == null || uri == null)
- return;
- final ContentResolver contentResolver = activity.getContentResolver();
-
- final Collection<CompletableFuture<Tunnel>> futureTunnels = new ArrayList<>();
- final List<Throwable> throwables = new ArrayList<>();
- Application.getAsyncWorker().supplyAsync(() -> {
- final String[] columns = {OpenableColumns.DISPLAY_NAME};
- String name = null;
- try (Cursor cursor = contentResolver.query(uri, columns,
- null, null, null)) {
- if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0))
- name = cursor.getString(0);
- }
- if (name == null)
- name = Uri.decode(uri.getLastPathSegment());
- int idx = name.lastIndexOf('/');
- if (idx >= 0) {
- if (idx >= name.length() - 1)
- throw new IllegalArgumentException(getResources().getString(R.string.illegal_filename_error, name));
- name = name.substring(idx + 1);
- }
- boolean isZip = name.toLowerCase(Locale.ENGLISH).endsWith(".zip");
- if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
- name = name.substring(0, name.length() - ".conf".length());
- else if (!isZip)
- throw new IllegalArgumentException(getResources().getString(R.string.bad_extension_error));
-
- if (isZip) {
- try (ZipInputStream zip = new ZipInputStream(contentResolver.openInputStream(uri));
- BufferedReader reader = new BufferedReader(new InputStreamReader(zip))) {
- ZipEntry entry;
- while ((entry = zip.getNextEntry()) != null) {
- if (entry.isDirectory())
- continue;
- name = entry.getName();
- idx = name.lastIndexOf('/');
- if (idx >= 0) {
- if (idx >= name.length() - 1)
- continue;
- name = name.substring(name.lastIndexOf('/') + 1);
- }
- if (name.toLowerCase(Locale.ENGLISH).endsWith(".conf"))
- name = name.substring(0, name.length() - ".conf".length());
- else
- continue;
- Config config = null;
- try {
- config = Config.parse(reader);
- } catch (Exception e) {
- throwables.add(e);
- }
- if (config != null)
- futureTunnels.add(Application.getTunnelManager().create(name, config).toCompletableFuture());
- }
- }
- } else {
- futureTunnels.add(Application.getTunnelManager().create(name,
- Config.parse(contentResolver.openInputStream(uri))).toCompletableFuture());
- }
-
- if (futureTunnels.isEmpty()) {
- if (throwables.size() == 1)
- throw throwables.get(0);
- else if (throwables.isEmpty())
- throw new IllegalArgumentException(getResources().getString(R.string.no_configs_error));
- }
-
- return CompletableFuture.allOf(futureTunnels.toArray(new CompletableFuture[futureTunnels.size()]));
- }).whenComplete((future, exception) -> {
- if (exception != null) {
- onTunnelImportFinished(Collections.emptyList(), Collections.singletonList(exception));
- } else {
- future.whenComplete((ignored1, ignored2) -> {
- final List<Tunnel> tunnels = new ArrayList<>(futureTunnels.size());
- for (final CompletableFuture<Tunnel> futureTunnel : futureTunnels) {
- Tunnel tunnel = null;
- try {
- tunnel = futureTunnel.getNow(null);
- } catch (final Exception e) {
- throwables.add(e);
- }
- if (tunnel != null)
- tunnels.add(tunnel);
- }
- onTunnelImportFinished(tunnels, throwables);
- });
- }
- });
- }
-
- @Override
- public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (savedInstanceState != null) {
- final Collection<Integer> checkedItems = savedInstanceState.getIntegerArrayList("CHECKED_ITEMS");
- if (checkedItems != null) {
- for (final Integer i : checkedItems)
- actionModeListener.setItemChecked(i, true);
- }
- }
- }
-
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
- switch (requestCode) {
- case REQUEST_IMPORT:
- if (resultCode == Activity.RESULT_OK && data != null)
- importTunnel(data.getData());
- return;
- case IntentIntegrator.REQUEST_CODE:
- final IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
- if (result != null && result.getContents() != null) {
- importTunnel(result.getContents());
- }
- return;
- default:
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- @SuppressWarnings("deprecation")
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- binding = TunnelListFragmentBinding.inflate(inflater, container, false);
-
- binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
- if (binding != null) {
- binding.createMenu.collapse();
- }
- return false;
- });
- binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
- binding.executePendingBindings();
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- public void onPause() {
- if (binding != null) {
- binding.createMenu.collapse();
- }
- super.onPause();
- }
-
- public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
- startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
- if (binding != null)
- binding.createMenu.collapse();
- }
-
- public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
- final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
- startActivityForResult(intent, REQUEST_IMPORT);
- if (binding != null)
- binding.createMenu.collapse();
- }
-
- public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
- final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
- intentIntegrator.setOrientationLocked(false);
- intentIntegrator.setBeepEnabled(false);
- intentIntegrator.setPrompt(getString(R.string.qr_code_hint));
- intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
-
- if (binding != null)
- binding.createMenu.collapse();
- }
-
- @Override
- public void onSaveInstanceState(final Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putIntegerArrayList("CHECKED_ITEMS", actionModeListener.getCheckedItems());
- }
-
- @Override
- public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
- if (binding == null)
- return;
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- if (newTunnel != null)
- viewForTunnel(newTunnel, tunnels).setSingleSelected(true);
- if (oldTunnel != null)
- viewForTunnel(oldTunnel, tunnels).setSingleSelected(false);
- });
- }
-
- private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
- final String message;
- if (throwable == null) {
- message = getResources().getQuantityString(R.plurals.delete_success, count, count);
- } else {
- final String error = ErrorMessages.get(throwable);
- message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
- Log.e(TAG, message, throwable);
- }
- if (binding != null) {
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
- }
-
- private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
- String message = null;
-
- for (final Throwable throwable : throwables) {
- final String error = ErrorMessages.get(throwable);
- message = getString(R.string.import_error, error);
- Log.e(TAG, message, throwable);
- }
-
- if (tunnels.size() == 1 && throwables.isEmpty())
- message = getString(R.string.import_success, tunnels.get(0).getName());
- else if (tunnels.isEmpty() && throwables.size() == 1)
- /* Use the exception message from above. */ ;
- else if (throwables.isEmpty())
- message = getResources().getQuantityString(R.plurals.import_total_success,
- tunnels.size(), tunnels.size());
- else if (!throwables.isEmpty())
- message = getResources().getQuantityString(R.plurals.import_partial_success,
- tunnels.size() + throwables.size(),
- tunnels.size(), tunnels.size() + throwables.size());
-
- if (binding != null)
- Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
- }
-
- @Override
- public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
- super.onViewStateRestored(savedInstanceState);
-
- if (binding == null) {
- return;
- }
-
- binding.setFragment(this);
- Application.getTunnelManager().getTunnels().thenAccept(binding::setTunnels);
- binding.setRowConfigurationHandler((ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TunnelListItemBinding, Tunnel>) (binding, tunnel, position) -> {
- binding.setFragment(this);
- binding.getRoot().setOnClickListener(clicked -> {
- if (actionMode == null) {
- setSelectedTunnel(tunnel);
- } else {
- actionModeListener.toggleItemChecked(position);
- }
- });
- binding.getRoot().setOnLongClickListener(clicked -> {
- actionModeListener.toggleItemChecked(position);
- return true;
- });
-
- if (actionMode != null)
- ((MultiselectableRelativeLayout) binding.getRoot()).setMultiSelected(actionModeListener.checkedItems.contains(position));
- else
- ((MultiselectableRelativeLayout) binding.getRoot()).setSingleSelected(getSelectedTunnel() == tunnel);
- });
- }
-
- private MultiselectableRelativeLayout viewForTunnel(final Tunnel tunnel, final List tunnels) {
- return (MultiselectableRelativeLayout) binding.tunnelList.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel)).itemView;
- }
-
- private final class ActionModeListener implements ActionMode.Callback {
- private final Collection<Integer> checkedItems = new HashSet<>();
-
- @Nullable private Resources resources;
-
- public ArrayList<Integer> getCheckedItems() {
- return new ArrayList<>(checkedItems);
- }
-
- @Override
- public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_action_delete:
- final Iterable<Integer> copyCheckedItems = new HashSet<>(checkedItems);
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- final Collection<Tunnel> tunnelsToDelete = new ArrayList<>();
- for (final Integer position : copyCheckedItems)
- tunnelsToDelete.add(tunnels.get(position));
-
- final CompletableFuture[] futures = StreamSupport.stream(tunnelsToDelete)
- .map(Tunnel::delete)
- .toArray(CompletableFuture[]::new);
- CompletableFuture.allOf(futures)
- .thenApply(x -> futures.length)
- .whenComplete(TunnelListFragment.this::onTunnelDeletionFinished);
-
- });
- checkedItems.clear();
- mode.finish();
- return true;
- case R.id.menu_action_select_all:
- Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
- for (int i = 0; i < tunnels.size(); ++i) {
- setItemChecked(i, true);
- }
- });
- return true;
- default:
- return false;
- }
- }
-
- @Override
- public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
- actionMode = mode;
- if (getActivity() != null) {
- resources = getActivity().getResources();
- }
- mode.getMenuInflater().inflate(R.menu.tunnel_list_action_mode, menu);
- binding.tunnelList.getAdapter().notifyDataSetChanged();
- return true;
- }
-
- @Override
- public void onDestroyActionMode(final ActionMode mode) {
- actionMode = null;
- resources = null;
- checkedItems.clear();
- binding.tunnelList.getAdapter().notifyDataSetChanged();
- }
-
- @Override
- public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
- updateTitle(mode);
- return false;
- }
-
- void setItemChecked(final int position, final boolean checked) {
- if (checked) {
- checkedItems.add(position);
- } else {
- checkedItems.remove(position);
- }
-
- final RecyclerView.Adapter adapter = binding == null ? null : binding.tunnelList.getAdapter();
-
- if (actionMode == null && !checkedItems.isEmpty() && getActivity() != null) {
- ((AppCompatActivity) getActivity()).startSupportActionMode(this);
- } else if (actionMode != null && checkedItems.isEmpty()) {
- actionMode.finish();
- }
-
- if (adapter != null)
- adapter.notifyItemChanged(position);
-
- updateTitle(actionMode);
- }
-
- void toggleItemChecked(final int position) {
- setItemChecked(position, !checkedItems.contains(position));
- }
-
- private void updateTitle(@Nullable final ActionMode mode) {
- if (mode == null) {
- return;
- }
-
- final int count = checkedItems.size();
- if (count == 0) {
- mode.setTitle("");
- } else {
- mode.setTitle(resources.getQuantityString(R.plurals.delete_title, count, count));
- }
- }
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/model/ApplicationData.java b/app/src/main/java/com/wireguard/android/model/ApplicationData.java
deleted file mode 100644
index 65edff90..00000000
--- a/app/src/main/java/com/wireguard/android/model/ApplicationData.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.model;
-
-import androidx.databinding.BaseObservable;
-import androidx.databinding.Bindable;
-import android.graphics.drawable.Drawable;
-
-import com.wireguard.android.BR;
-import com.wireguard.util.Keyed;
-
-public class ApplicationData extends BaseObservable implements Keyed<String> {
- private final Drawable icon;
- private final String name;
- private final String packageName;
- private boolean excludedFromTunnel;
-
- public ApplicationData(final Drawable icon, final String name, final String packageName, final boolean excludedFromTunnel) {
- this.icon = icon;
- this.name = name;
- this.packageName = packageName;
- this.excludedFromTunnel = excludedFromTunnel;
- }
-
- public Drawable getIcon() {
- return icon;
- }
-
- @Override
- public String getKey() {
- return name;
- }
-
- public String getName() {
- return name;
- }
-
- public String getPackageName() {
- return packageName;
- }
-
- @Bindable
- public boolean isExcludedFromTunnel() {
- return excludedFromTunnel;
- }
-
- public void setExcludedFromTunnel(final boolean excludedFromTunnel) {
- this.excludedFromTunnel = excludedFromTunnel;
- notifyPropertyChanged(BR.excludedFromTunnel);
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/model/Tunnel.java b/app/src/main/java/com/wireguard/android/model/Tunnel.java
deleted file mode 100644
index 49e78a22..00000000
--- a/app/src/main/java/com/wireguard/android/model/Tunnel.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.model;
-
-import androidx.databinding.BaseObservable;
-import androidx.databinding.Bindable;
-import androidx.annotation.Nullable;
-
-import com.wireguard.android.BR;
-import com.wireguard.android.util.ExceptionLoggers;
-import com.wireguard.config.Config;
-import com.wireguard.util.Keyed;
-
-import java.util.regex.Pattern;
-
-import java9.util.concurrent.CompletableFuture;
-import java9.util.concurrent.CompletionStage;
-
-/**
- * Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
- */
-
-public class Tunnel extends BaseObservable implements Keyed<String> {
- public static final int NAME_MAX_LENGTH = 15;
- private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
-
- private final TunnelManager manager;
- @Nullable private Config config;
- private String name;
- private State state;
- @Nullable private Statistics statistics;
-
- Tunnel(final TunnelManager manager, final String name,
- @Nullable final Config config, final State state) {
- this.manager = manager;
- this.name = name;
- this.config = config;
- this.state = state;
- }
-
- public static boolean isNameInvalid(final CharSequence name) {
- return !NAME_PATTERN.matcher(name).matches();
- }
-
- public CompletionStage<Void> delete() {
- return manager.delete(this);
- }
-
- @Bindable
- @Nullable
- public Config getConfig() {
- if (config == null)
- manager.getTunnelConfig(this).whenComplete(ExceptionLoggers.E);
- return config;
- }
-
- public CompletionStage<Config> getConfigAsync() {
- if (config == null)
- return manager.getTunnelConfig(this);
- return CompletableFuture.completedFuture(config);
- }
-
- @Override
- public String getKey() {
- return name;
- }
-
- @Bindable
- public String getName() {
- return name;
- }
-
- @Bindable
- public State getState() {
- return state;
- }
-
- public CompletionStage<State> getStateAsync() {
- return TunnelManager.getTunnelState(this);
- }
-
- @Bindable
- @Nullable
- public Statistics getStatistics() {
- // FIXME: Check age of statistics.
- if (statistics == null)
- TunnelManager.getTunnelStatistics(this).whenComplete(ExceptionLoggers.E);
- return statistics;
- }
-
- public CompletionStage<Statistics> getStatisticsAsync() {
- // FIXME: Check age of statistics.
- if (statistics == null)
- return TunnelManager.getTunnelStatistics(this);
- return CompletableFuture.completedFuture(statistics);
- }
-
- Config onConfigChanged(final Config config) {
- this.config = config;
- notifyPropertyChanged(BR.config);
- return config;
- }
-
- public String onNameChanged(final String name) {
- this.name = name;
- notifyPropertyChanged(BR.name);
- return name;
- }
-
- State onStateChanged(final State state) {
- if (state != State.UP)
- onStatisticsChanged(null);
- this.state = state;
- notifyPropertyChanged(BR.state);
- return state;
- }
-
- @Nullable
- Statistics onStatisticsChanged(@Nullable final Statistics statistics) {
- this.statistics = statistics;
- notifyPropertyChanged(BR.statistics);
- return statistics;
- }
-
- public CompletionStage<Config> setConfig(final Config config) {
- if (!config.equals(this.config))
- return manager.setTunnelConfig(this, config);
- return CompletableFuture.completedFuture(this.config);
- }
-
- public CompletionStage<String> setName(final String name) {
- if (!name.equals(this.name))
- return manager.setTunnelName(this, name);
- return CompletableFuture.completedFuture(this.name);
- }
-
- public CompletionStage<State> setState(final State state) {
- if (state != this.state)
- return manager.setTunnelState(this, state);
- return CompletableFuture.completedFuture(this.state);
- }
-
- public enum State {
- DOWN,
- TOGGLE,
- UP;
-
- public static State of(final boolean running) {
- return running ? UP : DOWN;
- }
- }
-
- public static class Statistics extends BaseObservable {
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/model/TunnelManager.java b/app/src/main/java/com/wireguard/android/model/TunnelManager.java
deleted file mode 100644
index 2deea6df..00000000
--- a/app/src/main/java/com/wireguard/android/model/TunnelManager.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.model;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import androidx.databinding.BaseObservable;
-import androidx.databinding.Bindable;
-import androidx.annotation.Nullable;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.BR;
-import com.wireguard.android.R;
-import com.wireguard.android.configStore.ConfigStore;
-import com.wireguard.android.model.Tunnel.State;
-import com.wireguard.android.model.Tunnel.Statistics;
-import com.wireguard.android.util.ExceptionLoggers;
-import com.wireguard.android.util.ObservableSortedKeyedArrayList;
-import com.wireguard.android.util.ObservableSortedKeyedList;
-import com.wireguard.config.Config;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Set;
-
-import java9.util.Comparators;
-import java9.util.concurrent.CompletableFuture;
-import java9.util.concurrent.CompletionStage;
-import java9.util.stream.Collectors;
-import java9.util.stream.StreamSupport;
-
-/**
- * Maintains and mediates changes to the set of available WireGuard tunnels,
- */
-
-public final class TunnelManager extends BaseObservable {
- private static final Comparator<String> COMPARATOR = Comparators.<String>thenComparing(
- String.CASE_INSENSITIVE_ORDER, Comparators.naturalOrder());
- private static final String KEY_LAST_USED_TUNNEL = "last_used_tunnel";
- private static final String KEY_RESTORE_ON_BOOT = "restore_on_boot";
- private static final String KEY_RUNNING_TUNNELS = "enabled_configs";
-
- private final CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> completableTunnels = new CompletableFuture<>();
- private final ConfigStore configStore;
- private final Context context = Application.get();
- private final ArrayList<CompletableFuture<Void>> delayedLoadRestoreTunnels = new ArrayList<>();
- private final ObservableSortedKeyedList<String, Tunnel> tunnels = new ObservableSortedKeyedArrayList<>(COMPARATOR);
- private boolean haveLoaded;
- @Nullable private Tunnel lastUsedTunnel;
-
- public TunnelManager(final ConfigStore configStore) {
- this.configStore = configStore;
- }
-
- static CompletionStage<State> getTunnelState(final Tunnel tunnel) {
- return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getState(tunnel))
- .thenApply(tunnel::onStateChanged);
- }
-
- static CompletionStage<Statistics> getTunnelStatistics(final Tunnel tunnel) {
- return Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().getStatistics(tunnel))
- .thenApply(tunnel::onStatisticsChanged);
- }
-
- private Tunnel addToList(final String name, @Nullable final Config config, final State state) {
- final Tunnel tunnel = new Tunnel(this, name, config, state);
- tunnels.add(tunnel);
- return tunnel;
- }
-
- public CompletionStage<Tunnel> create(final String name, @Nullable final Config config) {
- if (Tunnel.isNameInvalid(name))
- return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
- if (tunnels.containsKey(name)) {
- final String message = context.getString(R.string.tunnel_error_already_exists, name);
- return CompletableFuture.failedFuture(new IllegalArgumentException(message));
- }
- return Application.getAsyncWorker().supplyAsync(() -> configStore.create(name, config))
- .thenApply(savedConfig -> addToList(name, savedConfig, State.DOWN));
- }
-
- CompletionStage<Void> delete(final Tunnel tunnel) {
- final State originalState = tunnel.getState();
- final boolean wasLastUsed = tunnel == lastUsedTunnel;
- // Make sure nothing touches the tunnel.
- if (wasLastUsed)
- setLastUsedTunnel(null);
- tunnels.remove(tunnel);
- return Application.getAsyncWorker().runAsync(() -> {
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.DOWN);
- try {
- configStore.delete(tunnel.getName());
- } catch (final Exception e) {
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.UP);
- // Re-throw the exception to fail the completion.
- throw e;
- }
- }).whenComplete((x, e) -> {
- if (e == null)
- return;
- // Failure, put the tunnel back.
- tunnels.add(tunnel);
- if (wasLastUsed)
- setLastUsedTunnel(tunnel);
- });
- }
-
- @Bindable
- @Nullable
- public Tunnel getLastUsedTunnel() {
- return lastUsedTunnel;
- }
-
- CompletionStage<Config> getTunnelConfig(final Tunnel tunnel) {
- return Application.getAsyncWorker().supplyAsync(() -> configStore.load(tunnel.getName()))
- .thenApply(tunnel::onConfigChanged);
- }
-
- public CompletableFuture<ObservableSortedKeyedList<String, Tunnel>> getTunnels() {
- return completableTunnels;
- }
-
- public void onCreate() {
- Application.getAsyncWorker().supplyAsync(configStore::enumerate)
- .thenAcceptBoth(Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate()), this::onTunnelsLoaded)
- .whenComplete(ExceptionLoggers.E);
- }
-
- @SuppressWarnings("unchecked")
- private void onTunnelsLoaded(final Iterable<String> present, final Collection<String> running) {
- for (final String name : present)
- addToList(name, null, running.contains(name) ? State.UP : State.DOWN);
- final String lastUsedName = Application.getSharedPreferences().getString(KEY_LAST_USED_TUNNEL, null);
- if (lastUsedName != null)
- setLastUsedTunnel(tunnels.get(lastUsedName));
- final CompletableFuture<Void>[] toComplete;
- synchronized (delayedLoadRestoreTunnels) {
- haveLoaded = true;
- toComplete = delayedLoadRestoreTunnels.toArray(new CompletableFuture[delayedLoadRestoreTunnels.size()]);
- delayedLoadRestoreTunnels.clear();
- }
- restoreState(true).whenComplete((v, t) -> {
- for (final CompletableFuture<Void> f : toComplete) {
- if (t == null)
- f.complete(v);
- else
- f.completeExceptionally(t);
- }
- });
-
- completableTunnels.complete(tunnels);
- }
-
- public void refreshTunnelStates() {
- Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().enumerate())
- .thenAccept(running -> {
- for (final Tunnel tunnel : tunnels)
- tunnel.onStateChanged(running.contains(tunnel.getName()) ? State.UP : State.DOWN);
- })
- .whenComplete(ExceptionLoggers.E);
- }
-
- public CompletionStage<Void> restoreState(final boolean force) {
- if (!force && !Application.getSharedPreferences().getBoolean(KEY_RESTORE_ON_BOOT, false))
- return CompletableFuture.completedFuture(null);
- synchronized (delayedLoadRestoreTunnels) {
- if (!haveLoaded) {
- final CompletableFuture<Void> f = new CompletableFuture<>();
- delayedLoadRestoreTunnels.add(f);
- return f;
- }
- }
- final Set<String> previouslyRunning = Application.getSharedPreferences().getStringSet(KEY_RUNNING_TUNNELS, null);
- if (previouslyRunning == null)
- return CompletableFuture.completedFuture(null);
- return CompletableFuture.allOf(StreamSupport.stream(tunnels)
- .filter(tunnel -> previouslyRunning.contains(tunnel.getName()))
- .map(tunnel -> setTunnelState(tunnel, State.UP))
- .toArray(CompletableFuture[]::new));
- }
-
- public void saveState() {
- final Set<String> runningTunnels = StreamSupport.stream(tunnels)
- .filter(tunnel -> tunnel.getState() == State.UP)
- .map(Tunnel::getName)
- .collect(Collectors.toUnmodifiableSet());
- Application.getSharedPreferences().edit().putStringSet(KEY_RUNNING_TUNNELS, runningTunnels).apply();
- }
-
- private void setLastUsedTunnel(@Nullable final Tunnel tunnel) {
- if (tunnel == lastUsedTunnel)
- return;
- lastUsedTunnel = tunnel;
- notifyPropertyChanged(BR.lastUsedTunnel);
- if (tunnel != null)
- Application.getSharedPreferences().edit().putString(KEY_LAST_USED_TUNNEL, tunnel.getName()).apply();
- else
- Application.getSharedPreferences().edit().remove(KEY_LAST_USED_TUNNEL).apply();
- }
-
- CompletionStage<Config> setTunnelConfig(final Tunnel tunnel, final Config config) {
- return Application.getAsyncWorker().supplyAsync(() -> {
- final Config appliedConfig = Application.getBackend().applyConfig(tunnel, config);
- return configStore.save(tunnel.getName(), appliedConfig);
- }).thenApply(tunnel::onConfigChanged);
- }
-
- CompletionStage<String> setTunnelName(final Tunnel tunnel, final String name) {
- if (Tunnel.isNameInvalid(name))
- return CompletableFuture.failedFuture(new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name)));
- if (tunnels.containsKey(name)) {
- final String message = context.getString(R.string.tunnel_error_already_exists, name);
- return CompletableFuture.failedFuture(new IllegalArgumentException(message));
- }
- final State originalState = tunnel.getState();
- final boolean wasLastUsed = tunnel == lastUsedTunnel;
- // Make sure nothing touches the tunnel.
- if (wasLastUsed)
- setLastUsedTunnel(null);
- tunnels.remove(tunnel);
- return Application.getAsyncWorker().supplyAsync(() -> {
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.DOWN);
- configStore.rename(tunnel.getName(), name);
- final String newName = tunnel.onNameChanged(name);
- if (originalState == State.UP)
- Application.getBackend().setState(tunnel, State.UP);
- return newName;
- }).whenComplete((newName, e) -> {
- // On failure, we don't know what state the tunnel might be in. Fix that.
- if (e != null)
- getTunnelState(tunnel);
- // Add the tunnel back to the manager, under whatever name it thinks it has.
- tunnels.add(tunnel);
- if (wasLastUsed)
- setLastUsedTunnel(tunnel);
- });
- }
-
- CompletionStage<State> setTunnelState(final Tunnel tunnel, final State state) {
- // Ensure the configuration is loaded before trying to use it.
- return tunnel.getConfigAsync().thenCompose(x ->
- Application.getAsyncWorker().supplyAsync(() -> Application.getBackend().setState(tunnel, state))
- ).whenComplete((newState, e) -> {
- // Ensure onStateChanged is always called (failure or not), and with the correct state.
- tunnel.onStateChanged(e == null ? newState : tunnel.getState());
- if (e == null && newState == State.UP)
- setLastUsedTunnel(tunnel);
- saveState();
- });
- }
-
- public static final class IntentReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(final Context context, @Nullable final Intent intent) {
- final TunnelManager manager = Application.getTunnelManager();
- if (intent == null)
- return;
- final String action = intent.getAction();
- if (action == null)
- return;
-
- if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES".equals(action)) {
- manager.refreshTunnelStates();
- return;
- }
-
- /* We disable the below, for now, as the security model of allowing this
- * might take a bit more consideration.
- */
- if (true)
- return;
-
- final State state;
- if ("com.wireguard.android.action.SET_TUNNEL_UP".equals(action))
- state = State.UP;
- else if ("com.wireguard.android.action.SET_TUNNEL_DOWN".equals(action))
- state = State.DOWN;
- else
- return;
-
- final String tunnelName = intent.getStringExtra("tunnel");
- if (tunnelName == null)
- return;
- manager.getTunnels().thenAccept(tunnels -> {
- final Tunnel tunnel = tunnels.get(tunnelName);
- if (tunnel == null)
- return;
- manager.setTunnelState(tunnel, state);
- });
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java
deleted file mode 100644
index 565854b4..00000000
--- a/app/src/main/java/com/wireguard/android/preference/LogExporterPreference.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.preference;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import androidx.annotation.Nullable;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.preference.Preference;
-
-import android.util.AttributeSet;
-import android.util.Log;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.util.DownloadsFileSaver;
-import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.util.FragmentUtils;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-
-/**
- * Preference implementing a button that asynchronously exports logs.
- */
-
-public class LogExporterPreference extends Preference {
- private static final String TAG = "WireGuard/" + LogExporterPreference.class.getSimpleName();
-
- @Nullable private String exportedFilePath;
-
- public LogExporterPreference(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- private void exportLog() {
- Application.getAsyncWorker().supplyAsync(() -> {
- DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-log.txt", "text/plain", true);
- try {
- final Process process = Runtime.getRuntime().exec(new String[]{
- "logcat", "-b", "all", "-d", "-v", "threadtime", "*:V"});
- try (final BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
- final BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())))
- {
- String line;
- while ((line = stdout.readLine()) != null) {
- outputFile.getOutputStream().write(line.getBytes());
- outputFile.getOutputStream().write('\n');
- }
- outputFile.getOutputStream().close();
- stdout.close();
- if (process.waitFor() != 0) {
- final StringBuilder errors = new StringBuilder();
- errors.append(R.string.logcat_error);
- while ((line = stderr.readLine()) != null)
- errors.append(line);
- throw new Exception(errors.toString());
- }
- }
- } catch (final Exception e) {
- outputFile.delete();
- throw e;
- }
- return outputFile.getFileName();
- }).whenComplete(this::exportLogComplete);
- }
-
- private void exportLogComplete(final String filePath, @Nullable final Throwable throwable) {
- if (throwable != null) {
- final String error = ErrorMessages.get(throwable);
- final String message = getContext().getString(R.string.log_export_error, error);
- Log.e(TAG, message, throwable);
- Snackbar.make(
- FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
- message, Snackbar.LENGTH_LONG).show();
- setEnabled(true);
- } else {
- exportedFilePath = filePath;
- notifyChanged();
- }
- }
-
- @Override
- public CharSequence getSummary() {
- return exportedFilePath == null ?
- getContext().getString(R.string.log_export_summary) :
- getContext().getString(R.string.log_export_success, exportedFilePath);
- }
-
- @Override
- public CharSequence getTitle() {
- return getContext().getString(R.string.log_export_title);
- }
-
- @Override
- protected void onClick() {
- FragmentUtils.getPrefActivity(this).ensurePermissions(
- new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
- (permissions, granted) -> {
- if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
- setEnabled(false);
- exportLog();
- }
- });
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java b/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java
deleted file mode 100644
index a04bed76..00000000
--- a/app/src/main/java/com/wireguard/android/preference/ModuleDownloaderPreference.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright © 2019 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.system.OsConstants;
-import android.util.AttributeSet;
-import android.widget.Toast;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.util.ModuleLoader;
-import com.wireguard.android.util.ToolsInstaller;
-
-import androidx.annotation.Nullable;
-import androidx.preference.Preference;
-
-public class ModuleDownloaderPreference extends Preference {
- private State state = State.INITIAL;
-
- public ModuleDownloaderPreference(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public CharSequence getSummary() {
- return getContext().getString(state.messageResourceId);
- }
-
- @Override
- public CharSequence getTitle() {
- return getContext().getString(R.string.module_installer_title);
- }
-
- @Override
- protected void onClick() {
- setState(State.WORKING);
- Application.getAsyncWorker().supplyAsync(Application.getModuleLoader()::download).whenComplete(this::onDownloadResult);
- }
-
- private void onDownloadResult(final Integer result, @Nullable final Throwable throwable) {
- if (throwable != null) {
- setState(State.FAILURE);
- Toast.makeText(getContext(), throwable.getMessage(), Toast.LENGTH_LONG).show();
- } else if (result == OsConstants.ENOENT)
- setState(State.NOTFOUND);
- else if (result == OsConstants.EXIT_SUCCESS) {
- setState(State.SUCCESS);
- Application.getAsyncWorker().runAsync(() -> {
- Thread.sleep(1000 * 5);
- Intent i = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
- if (i == null)
- return;
- i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Application.get().startActivity(i);
- System.exit(0);
- });
- } else
- setState(State.FAILURE);
- }
-
- private void setState(final State state) {
- if (this.state == state)
- return;
- this.state = state;
- if (isEnabled() != state.shouldEnableView)
- setEnabled(state.shouldEnableView);
- notifyChanged();
- }
-
- private enum State {
- INITIAL(R.string.module_installer_initial, true),
- FAILURE(R.string.module_installer_error, true),
- WORKING(R.string.module_installer_working, false),
- SUCCESS(R.string.module_installer_success, false),
- NOTFOUND(R.string.module_installer_not_found, false);
-
- private final int messageResourceId;
- private final boolean shouldEnableView;
-
- State(final int messageResourceId, final boolean shouldEnableView) {
- this.messageResourceId = messageResourceId;
- this.shouldEnableView = shouldEnableView;
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java b/app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java
deleted file mode 100644
index 78a7497b..00000000
--- a/app/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.preference;
-
-import android.content.Context;
-import androidx.annotation.Nullable;
-import androidx.preference.Preference;
-import android.util.AttributeSet;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.util.ToolsInstaller;
-
-/**
- * Preference implementing a button that asynchronously runs {@code ToolsInstaller} and displays the
- * result as the preference summary.
- */
-
-public class ToolsInstallerPreference extends Preference {
- private State state = State.INITIAL;
-
- public ToolsInstallerPreference(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public CharSequence getSummary() {
- return getContext().getString(state.messageResourceId);
- }
-
- @Override
- public CharSequence getTitle() {
- return getContext().getString(R.string.tools_installer_title);
- }
-
- @Override
- public void onAttached() {
- super.onAttached();
- Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::areInstalled).whenComplete(this::onCheckResult);
- }
-
- private void onCheckResult(final int state, @Nullable final Throwable throwable) {
- if (throwable != null || state == ToolsInstaller.ERROR)
- setState(State.INITIAL);
- else if ((state & ToolsInstaller.YES) == ToolsInstaller.YES)
- setState(State.ALREADY);
- else if ((state & (ToolsInstaller.MAGISK | ToolsInstaller.NO)) == (ToolsInstaller.MAGISK | ToolsInstaller.NO))
- setState(State.INITIAL_MAGISK);
- else if ((state & (ToolsInstaller.SYSTEM | ToolsInstaller.NO)) == (ToolsInstaller.SYSTEM | ToolsInstaller.NO))
- setState(State.INITIAL_SYSTEM);
- else
- setState(State.INITIAL);
- }
-
- @Override
- protected void onClick() {
- setState(State.WORKING);
- Application.getAsyncWorker().supplyAsync(Application.getToolsInstaller()::install).whenComplete(this::onInstallResult);
- }
-
- private void onInstallResult(final Integer result, @Nullable final Throwable throwable) {
- if (throwable != null)
- setState(State.FAILURE);
- else if ((result & (ToolsInstaller.YES | ToolsInstaller.MAGISK)) == (ToolsInstaller.YES | ToolsInstaller.MAGISK))
- setState(State.SUCCESS_MAGISK);
- else if ((result & (ToolsInstaller.YES | ToolsInstaller.SYSTEM)) == (ToolsInstaller.YES | ToolsInstaller.SYSTEM))
- setState(State.SUCCESS_SYSTEM);
- else
- setState(State.FAILURE);
- }
-
- private void setState(final State state) {
- if (this.state == state)
- return;
- this.state = state;
- if (isEnabled() != state.shouldEnableView)
- setEnabled(state.shouldEnableView);
- notifyChanged();
- }
-
- private enum State {
- INITIAL(R.string.tools_installer_initial, true),
- ALREADY(R.string.tools_installer_already, false),
- FAILURE(R.string.tools_installer_failure, true),
- WORKING(R.string.tools_installer_working, false),
- INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
- SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
- INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
- SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
-
- private final int messageResourceId;
- private final boolean shouldEnableView;
-
- State(final int messageResourceId, final boolean shouldEnableView) {
- this.messageResourceId = messageResourceId;
- this.shouldEnableView = shouldEnableView;
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/preference/VersionPreference.java b/app/src/main/java/com/wireguard/android/preference/VersionPreference.java
deleted file mode 100644
index a0a5d1ff..00000000
--- a/app/src/main/java/com/wireguard/android/preference/VersionPreference.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright © 2017-2019 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 androidx.annotation.Nullable;
-import androidx.preference.Preference;
-import android.util.AttributeSet;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.BuildConfig;
-import com.wireguard.android.R;
-
-import java.util.Locale;
-
-public class VersionPreference extends Preference {
- @Nullable private String versionSummary;
-
- public VersionPreference(final Context context, final AttributeSet attrs) {
- super(context, attrs);
-
- Application.getBackendAsync().thenAccept(backend -> {
- versionSummary = getContext().getString(R.string.version_summary_checking, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
- Application.getAsyncWorker().supplyAsync(backend::getVersion).whenComplete((version, exception) -> {
- versionSummary = exception == null
- ? getContext().getString(R.string.version_summary, backend.getTypePrettyName(), version)
- : getContext().getString(R.string.version_summary_unknown, backend.getTypePrettyName().toLowerCase(Locale.ENGLISH));
- notifyChanged();
- });
- });
- }
-
- @Nullable
- @Override
- public CharSequence getSummary() {
- return versionSummary;
- }
-
- @Override
- public CharSequence getTitle() {
- return getContext().getString(R.string.version_title, BuildConfig.VERSION_NAME);
- }
-
- @Override
- protected void onClick() {
- final Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(Uri.parse("https://www.wireguard.com/"));
- try {
- getContext().startActivity(intent);
- } catch (final ActivityNotFoundException ignored) {
- }
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java b/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java
deleted file mode 100644
index efda91bb..00000000
--- a/app/src/main/java/com/wireguard/android/preference/ZipExporterPreference.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.preference;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import androidx.annotation.Nullable;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.preference.Preference;
-import android.util.AttributeSet;
-import android.util.Log;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.util.DownloadsFileSaver;
-import com.wireguard.android.util.DownloadsFileSaver.DownloadsFile;
-import com.wireguard.android.util.ErrorMessages;
-import com.wireguard.android.util.FragmentUtils;
-import com.wireguard.config.Config;
-
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-import java9.util.concurrent.CompletableFuture;
-
-/**
- * Preference implementing a button that asynchronously exports config zips.
- */
-
-public class ZipExporterPreference extends Preference {
- private static final String TAG = "WireGuard/" + ZipExporterPreference.class.getSimpleName();
-
- @Nullable private String exportedFilePath;
-
- public ZipExporterPreference(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- private void exportZip() {
- Application.getTunnelManager().getTunnels().thenAccept(this::exportZip);
- }
-
- private void exportZip(final List<Tunnel> tunnels) {
- final List<CompletableFuture<Config>> futureConfigs = new ArrayList<>(tunnels.size());
- for (final Tunnel tunnel : tunnels)
- futureConfigs.add(tunnel.getConfigAsync().toCompletableFuture());
- if (futureConfigs.isEmpty()) {
- exportZipComplete(null, new IllegalArgumentException(
- getContext().getString(R.string.no_tunnels_error)));
- return;
- }
- CompletableFuture.allOf(futureConfigs.toArray(new CompletableFuture[futureConfigs.size()]))
- .whenComplete((ignored1, exception) -> Application.getAsyncWorker().supplyAsync(() -> {
- if (exception != null)
- throw exception;
- DownloadsFile outputFile = DownloadsFileSaver.save(getContext(), "wireguard-export.zip", "application/zip", true);
- try (ZipOutputStream zip = new ZipOutputStream(outputFile.getOutputStream())) {
- for (int i = 0; i < futureConfigs.size(); ++i) {
- zip.putNextEntry(new ZipEntry(tunnels.get(i).getName() + ".conf"));
- zip.write(futureConfigs.get(i).getNow(null).
- toWgQuickString().getBytes(StandardCharsets.UTF_8));
- }
- zip.closeEntry();
- } catch (final Exception e) {
- outputFile.delete();
- throw e;
- }
- return outputFile.getFileName();
- }).whenComplete(this::exportZipComplete));
- }
-
- private void exportZipComplete(@Nullable final String filePath, @Nullable final Throwable throwable) {
- if (throwable != null) {
- final String error = ErrorMessages.get(throwable);
- final String message = getContext().getString(R.string.zip_export_error, error);
- Log.e(TAG, message, throwable);
- Snackbar.make(
- FragmentUtils.getPrefActivity(this).findViewById(android.R.id.content),
- message, Snackbar.LENGTH_LONG).show();
- setEnabled(true);
- } else {
- exportedFilePath = filePath;
- notifyChanged();
- }
- }
-
- @Override
- public CharSequence getSummary() {
- return exportedFilePath == null ?
- getContext().getString(R.string.zip_export_summary) :
- getContext().getString(R.string.zip_export_success, exportedFilePath);
- }
-
- @Override
- public CharSequence getTitle() {
- return getContext().getString(R.string.zip_export_title);
- }
-
- @Override
- protected void onClick() {
- FragmentUtils.getPrefActivity(this).ensurePermissions(
- new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
- (permissions, granted) -> {
- if (granted.length > 0 && granted[0] == PackageManager.PERMISSION_GRANTED) {
- setEnabled(false);
- exportZip();
- }
- });
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/util/AsyncWorker.java b/app/src/main/java/com/wireguard/android/util/AsyncWorker.java
deleted file mode 100644
index 1d041851..00000000
--- a/app/src/main/java/com/wireguard/android/util/AsyncWorker.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import android.os.Handler;
-
-import java.util.concurrent.Executor;
-
-import java9.util.concurrent.CompletableFuture;
-import java9.util.concurrent.CompletionStage;
-
-/**
- * Helper class for running asynchronous tasks and ensuring they are completed on the main thread.
- */
-
-public class AsyncWorker {
- private final Executor executor;
- private final Handler handler;
-
- public AsyncWorker(final Executor executor, final Handler handler) {
- this.executor = executor;
- this.handler = handler;
- }
-
- public CompletionStage<Void> runAsync(final AsyncRunnable<?> runnable) {
- final CompletableFuture<Void> future = new CompletableFuture<>();
- executor.execute(() -> {
- try {
- runnable.run();
- handler.post(() -> future.complete(null));
- } catch (final Throwable t) {
- handler.post(() -> future.completeExceptionally(t));
- }
- });
- return future;
- }
-
- public <T> CompletionStage<T> supplyAsync(final AsyncSupplier<T, ?> supplier) {
- final CompletableFuture<T> future = new CompletableFuture<>();
- executor.execute(() -> {
- try {
- final T result = supplier.get();
- handler.post(() -> future.complete(result));
- } catch (final Throwable t) {
- handler.post(() -> future.completeExceptionally(t));
- }
- });
- return future;
- }
-
- @FunctionalInterface
- public interface AsyncRunnable<E extends Throwable> {
- void run() throws E;
- }
-
- @FunctionalInterface
- public interface AsyncSupplier<T, E extends Throwable> {
- T get() throws E;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ClipboardUtils.java b/app/src/main/java/com/wireguard/android/util/ClipboardUtils.java
deleted file mode 100644
index 0df5e96a..00000000
--- a/app/src/main/java/com/wireguard/android/util/ClipboardUtils.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright © 2017-2019 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.content.Context;
-import com.google.android.material.snackbar.Snackbar;
-import android.view.View;
-import android.widget.TextView;
-
-/**
- * Standalone utilities for interacting with the system clipboard.
- */
-
-public final class ClipboardUtils {
- private ClipboardUtils() {
- // Prevent instantiation
- }
-
- public static void copyTextView(final View view) {
- if (!(view instanceof TextView))
- return;
- final CharSequence text = ((TextView) view).getText();
- if (text == null || text.length() == 0)
- return;
- final Object service = view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- if (!(service instanceof ClipboardManager))
- return;
- final CharSequence description = view.getContentDescription();
- ((ClipboardManager) service).setPrimaryClip(ClipData.newPlainText(description, text));
- Snackbar.make(view, description + " copied to clipboard", Snackbar.LENGTH_LONG).show();
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java b/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java
deleted file mode 100644
index efe09f3f..00000000
--- a/app/src/main/java/com/wireguard/android/util/DownloadsFileSaver.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright © 2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.provider.MediaStore.MediaColumns;
-
-import com.wireguard.android.R;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-public class DownloadsFileSaver {
-
- public static class DownloadsFile {
- private Context context;
- private OutputStream outputStream;
- private String fileName;
- private Uri uri;
-
- private DownloadsFile(final Context context, final OutputStream outputStream, final String fileName, final Uri uri) {
- this.context = context;
- this.outputStream = outputStream;
- this.fileName = fileName;
- this.uri = uri;
- }
-
- public OutputStream getOutputStream() { return outputStream; }
- public String getFileName() { return fileName; }
-
- public void delete() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
- context.getContentResolver().delete(uri, null, null);
- else
- new File(fileName).delete();
- }
- }
-
- public static DownloadsFile save(final Context context, final String name, final String mimeType, final boolean overwriteExisting) throws Exception {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- final ContentResolver contentResolver = context.getContentResolver();
- if (overwriteExisting)
- contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), new String[]{name});
- final ContentValues contentValues = new ContentValues();
- contentValues.put(MediaColumns.DISPLAY_NAME, name);
- contentValues.put(MediaColumns.MIME_TYPE, mimeType);
- final Uri contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
- if (contentUri == null)
- throw new IOException(context.getString(R.string.create_downloads_file_error));
- final OutputStream contentStream = contentResolver.openOutputStream(contentUri);
- if (contentStream == null)
- throw new IOException(context.getString(R.string.create_downloads_file_error));
- Cursor cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DATA}, null, null, null);
- String path = null;
- if (cursor != null) {
- try {
- if (cursor.moveToFirst())
- path = cursor.getString(0);
- } finally {
- cursor.close();
- }
- }
- if (path == null) {
- path = "Download/";
- cursor = contentResolver.query(contentUri, new String[]{MediaColumns.DISPLAY_NAME}, null, null, null);
- if (cursor != null) {
- try {
- if (cursor.moveToFirst())
- path += cursor.getString(0);
- } finally {
- cursor.close();
- }
- }
- }
- return new DownloadsFile(context, contentStream, path, contentUri);
- } else {
- final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- final File file = new File(path, name);
- if (!path.isDirectory() && !path.mkdirs())
- throw new IOException(context.getString(R.string.create_output_dir_error));
- return new DownloadsFile(context, new FileOutputStream(file), file.getAbsolutePath(), null);
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ErrorMessages.java b/app/src/main/java/com/wireguard/android/util/ErrorMessages.java
deleted file mode 100644
index ee9cb12a..00000000
--- a/app/src/main/java/com/wireguard/android/util/ErrorMessages.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import android.content.res.Resources;
-import androidx.annotation.Nullable;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.R;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.BadConfigException.Location;
-import com.wireguard.config.BadConfigException.Reason;
-import com.wireguard.config.InetEndpoint;
-import com.wireguard.config.InetNetwork;
-import com.wireguard.config.ParseException;
-import com.wireguard.crypto.Key.Format;
-import com.wireguard.crypto.KeyFormatException;
-import com.wireguard.crypto.KeyFormatException.Type;
-
-import java.net.InetAddress;
-import java.util.EnumMap;
-import java.util.Map;
-
-import java9.util.Maps;
-
-public final class ErrorMessages {
- private static final Map<Reason, Integer> BCE_REASON_MAP = new EnumMap<>(Maps.of(
- Reason.INVALID_KEY, R.string.bad_config_reason_invalid_key,
- Reason.INVALID_NUMBER, R.string.bad_config_reason_invalid_number,
- Reason.INVALID_VALUE, R.string.bad_config_reason_invalid_value,
- Reason.MISSING_ATTRIBUTE, R.string.bad_config_reason_missing_attribute,
- Reason.MISSING_SECTION, R.string.bad_config_reason_missing_section,
- Reason.MISSING_VALUE, R.string.bad_config_reason_missing_value,
- Reason.SYNTAX_ERROR, R.string.bad_config_reason_syntax_error,
- Reason.UNKNOWN_ATTRIBUTE, R.string.bad_config_reason_unknown_attribute,
- Reason.UNKNOWN_SECTION, R.string.bad_config_reason_unknown_section
- ));
- private static final Map<Format, Integer> KFE_FORMAT_MAP = new EnumMap<>(Maps.of(
- Format.BASE64, R.string.key_length_explanation_base64,
- Format.BINARY, R.string.key_length_explanation_binary,
- Format.HEX, R.string.key_length_explanation_hex
- ));
- private static final Map<Type, Integer> KFE_TYPE_MAP = new EnumMap<>(Maps.of(
- Type.CONTENTS, R.string.key_contents_error,
- Type.LENGTH, R.string.key_length_error
- ));
- private static final Map<Class, Integer> PE_CLASS_MAP = Maps.of(
- InetAddress.class, R.string.parse_error_inet_address,
- InetEndpoint.class, R.string.parse_error_inet_endpoint,
- InetNetwork.class, R.string.parse_error_inet_network,
- Integer.class, R.string.parse_error_integer
- );
-
- private ErrorMessages() {
- // Prevent instantiation
- }
-
- public static String get(@Nullable final Throwable throwable) {
- final Resources resources = Application.get().getResources();
- if (throwable == null)
- return resources.getString(R.string.unknown_error);
- final Throwable rootCause = rootCause(throwable);
- final String message;
- if (rootCause instanceof BadConfigException) {
- final BadConfigException bce = (BadConfigException) rootCause;
- final String reason = getBadConfigExceptionReason(resources, bce);
- final String context = bce.getLocation() == Location.TOP_LEVEL ?
- resources.getString(R.string.bad_config_context_top_level,
- bce.getSection().getName()) :
- resources.getString(R.string.bad_config_context,
- bce.getSection().getName(),
- bce.getLocation().getName());
- final String explanation = getBadConfigExceptionExplanation(resources, bce);
- message = resources.getString(R.string.bad_config_error, reason, context) + explanation;
- } else if (rootCause.getMessage() != null) {
- message = rootCause.getMessage();
- } else {
- final String errorType = rootCause.getClass().getSimpleName();
- message = resources.getString(R.string.generic_error, errorType);
- }
- return message;
- }
-
- private static String getBadConfigExceptionExplanation(final Resources resources,
- final BadConfigException bce) {
- if (bce.getCause() instanceof KeyFormatException) {
- final KeyFormatException kfe = (KeyFormatException) bce.getCause();
- if (kfe.getType() == Type.LENGTH)
- return resources.getString(KFE_FORMAT_MAP.get(kfe.getFormat()));
- } else if (bce.getCause() instanceof ParseException) {
- final ParseException pe = (ParseException) bce.getCause();
- if (pe.getMessage() != null)
- return ": " + pe.getMessage();
- } else if (bce.getLocation() == Location.LISTEN_PORT) {
- return resources.getString(R.string.bad_config_explanation_udp_port);
- } else if (bce.getLocation() == Location.MTU) {
- return resources.getString(R.string.bad_config_explanation_positive_number);
- } else if (bce.getLocation() == Location.PERSISTENT_KEEPALIVE) {
- return resources.getString(R.string.bad_config_explanation_pka);
- }
- return "";
- }
-
- private static String getBadConfigExceptionReason(final Resources resources,
- final BadConfigException bce) {
- if (bce.getCause() instanceof KeyFormatException) {
- final KeyFormatException kfe = (KeyFormatException) bce.getCause();
- return resources.getString(KFE_TYPE_MAP.get(kfe.getType()));
- } else if (bce.getCause() instanceof ParseException) {
- final ParseException pe = (ParseException) bce.getCause();
- final String type = resources.getString(PE_CLASS_MAP.containsKey(pe.getParsingClass()) ?
- PE_CLASS_MAP.get(pe.getParsingClass()) : R.string.parse_error_generic);
- return resources.getString(R.string.parse_error_reason, type, pe.getText());
- }
- return resources.getString(BCE_REASON_MAP.get(bce.getReason()), bce.getText());
- }
-
- private static Throwable rootCause(final Throwable throwable) {
- Throwable cause = throwable;
- while (cause.getCause() != null) {
- if (cause instanceof BadConfigException)
- break;
- cause = cause.getCause();
- }
- return cause;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java b/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java
deleted file mode 100644
index 5c7a38c0..00000000
--- a/app/src/main/java/com/wireguard/android/util/ExceptionLoggers.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import androidx.annotation.Nullable;
-import android.util.Log;
-
-import java9.util.function.BiConsumer;
-
-/**
- * Helpers for logging exceptions from asynchronous tasks. These can be passed to
- * {@code CompletionStage.whenComplete()} at the end of an asynchronous future chain.
- */
-
-public enum ExceptionLoggers implements BiConsumer<Object, Throwable> {
- D(Log.DEBUG),
- E(Log.ERROR);
-
- private static final String TAG = "WireGuard/" + ExceptionLoggers.class.getSimpleName();
- private final int priority;
-
- ExceptionLoggers(final int priority) {
- this.priority = priority;
- }
-
- @Override
- public void accept(final Object result, @Nullable final Throwable throwable) {
- if (throwable != null)
- Log.println(Log.ERROR, TAG, Log.getStackTraceString(throwable));
- else if (priority <= Log.DEBUG)
- Log.println(priority, TAG, "Future completed successfully");
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/FragmentUtils.java b/app/src/main/java/com/wireguard/android/util/FragmentUtils.java
deleted file mode 100644
index 5fb9a3bc..00000000
--- a/app/src/main/java/com/wireguard/android/util/FragmentUtils.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-package com.wireguard.android.util;
-
-import android.content.Context;
-import androidx.preference.Preference;
-import android.view.ContextThemeWrapper;
-
-import com.wireguard.android.activity.SettingsActivity;
-
-public final class FragmentUtils {
- private FragmentUtils() {
- // Prevent instantiation
- }
-
- public static SettingsActivity getPrefActivity(final Preference preference) {
- final Context context = preference.getContext();
- if (context instanceof ContextThemeWrapper) {
- if (context instanceof SettingsActivity) {
- return ((SettingsActivity) context);
- }
- }
- return null;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ModuleLoader.java b/app/src/main/java/com/wireguard/android/util/ModuleLoader.java
deleted file mode 100644
index 21ff9c77..00000000
--- a/app/src/main/java/com/wireguard/android/util/ModuleLoader.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright © 2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import android.content.Context;
-import android.system.OsConstants;
-import android.util.Base64;
-
-import com.wireguard.android.Application;
-import com.wireguard.android.BuildConfig;
-import com.wireguard.android.util.RootShell.NoRootException;
-
-import net.i2p.crypto.eddsa.EdDSAEngine;
-import net.i2p.crypto.eddsa.EdDSAPublicKey;
-import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
-import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
-import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidParameterException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.Signature;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-
-public class ModuleLoader {
- private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
- private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig";
- private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
- private static final String MODULE_NAME = "wireguard-%s.ko";
-
- private final File moduleDir;
- private final File tmpDir;
-
- public ModuleLoader(final Context context) {
- moduleDir = new File(context.getCacheDir(), "kmod");
- tmpDir = new File(context.getCacheDir(), "tmp");
- }
-
- public boolean moduleMightExist() {
- return moduleDir.exists() && moduleDir.isDirectory();
- }
-
- public void loadModule() throws IOException, NoRootException {
- Application.getRootShell().run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
- }
-
- public boolean isModuleLoaded() {
- return new File("/sys/module/wireguard").exists();
- }
-
- private static final class Sha256Digest {
- private byte[] bytes;
- private Sha256Digest(final String hex) {
- if (hex.length() != 64)
- throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
- bytes = new byte[32];
- for (int i = 0; i < 32; ++i)
- bytes[i] = (byte)Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
- }
- }
-
- @Nullable
- private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
- final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
-
- if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
- return null;
-
- final String[] lines = signifyDigest.split("\n", 3);
- if (lines.length != 3)
- return null;
- if (!lines[0].startsWith("untrusted comment: "))
- return null;
-
- final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
- if (signatureBytes == null || signatureBytes.length != 64 + 10)
- return null;
- for (int i = 0; i < 10; ++i) {
- if (signatureBytes[i] != publicKeyBytes[i])
- return null;
- }
-
- try {
- EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
- Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
- byte[] rawPublicKeyBytes = new byte[32];
- System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
- signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
- signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
- if (!signature.verify(signatureBytes, 10, 64))
- return null;
- } catch (final Exception ignored) {
- return null;
- }
-
- Map<String, Sha256Digest> hashes = new HashMap<>();
- for (final String line : lines[2].split("\n")) {
- final String[] components = line.split(" ", 2);
- if (components.length != 2)
- return null;
- try {
- hashes.put(components[1], new Sha256Digest(components[0]));
- } catch (final Exception ignored) {
- return null;
- }
- }
- return hashes;
- }
-
- public Integer download() throws IOException, NoRootException, NoSuchAlgorithmException {
- final List<String> output = new ArrayList<>();
- Application.getRootShell().run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
- if (output.size() != 1 || output.get(0).length() != 64)
- throw new InvalidParameterException("Invalid sha256 of /proc/version");
- final String moduleName = String.format(MODULE_NAME, output.get(0));
- final String userAgent = String.format("WireGuard/%s (Android)", BuildConfig.VERSION_NAME); //TODO: expand a bit
-
- HttpURLConnection connection = (HttpURLConnection)new URL(MODULE_LIST_URL).openConnection();
- connection.setRequestProperty("User-Agent", userAgent);
- connection.connect();
- if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
- throw new IOException("Hash list could not be found");
- byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
- int len;
- try (final InputStream inputStream = connection.getInputStream()) {
- len = inputStream.read(input);
- }
- if (len <= 0)
- throw new IOException("Hash list was empty");
- final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8));
- if (modules == null)
- throw new InvalidParameterException("The signature did not verify or invalid hash list format");
- if (!modules.containsKey(moduleName))
- return OsConstants.ENOENT;
- connection = (HttpURLConnection)new URL(String.format(MODULE_URL, moduleName)).openConnection();
- connection.setRequestProperty("User-Agent", userAgent);
- connection.connect();
- if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
- throw new IOException("Module file could not be found, despite being on hash list");
-
- tmpDir.mkdirs();
- moduleDir.mkdir();
- File tempFile = null;
- try {
- tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- try (final InputStream inputStream = connection.getInputStream();
- final OutputStream outputStream = new FileOutputStream(tempFile)) {
- int total = 0;
- while ((len = inputStream.read(input)) > 0) {
- total += len;
- if (total > 1024 * 1024 * 15 /* 15 MiB */)
- throw new IOException("File too big");
- outputStream.write(input, 0, len);
- digest.update(input, 0, len);
- }
- }
- if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes))
- throw new IOException("Incorrect file hash");
-
- if (!tempFile.renameTo(new File(moduleDir, moduleName)))
- throw new IOException("Unable to rename to final destination");
- } finally {
- if (tempFile != null)
- tempFile.delete();
- }
- return OsConstants.EXIT_SUCCESS;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java b/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java
deleted file mode 100644
index 0ba02184..00000000
--- a/app/src/main/java/com/wireguard/android/util/ObservableKeyedArrayList.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import androidx.databinding.ObservableArrayList;
-import androidx.annotation.Nullable;
-
-import com.wireguard.util.Keyed;
-
-import java.util.Collection;
-import java.util.ListIterator;
-import java.util.Objects;
-
-/**
- * ArrayList that allows looking up elements by some key property. As the key property must always
- * be retrievable, this list cannot hold {@code null} elements. Because this class places no
- * restrictions on the order or duplication of keys, lookup by key, as well as all list modification
- * operations, require O(n) time.
- */
-
-public class ObservableKeyedArrayList<K, E extends Keyed<? extends K>>
- extends ObservableArrayList<E> implements ObservableKeyedList<K, E> {
- @Override
- public boolean add(@Nullable final E e) {
- if (e == null)
- throw new NullPointerException("Trying to add a null element");
- return super.add(e);
- }
-
- @Override
- public void add(final int index, @Nullable final E e) {
- if (e == null)
- throw new NullPointerException("Trying to add a null element");
- super.add(index, e);
- }
-
- @Override
- public boolean addAll(final Collection<? extends E> c) {
- if (c.contains(null))
- throw new NullPointerException("Trying to add a collection with null element(s)");
- return super.addAll(c);
- }
-
- @Override
- public boolean addAll(final int index, final Collection<? extends E> c) {
- if (c.contains(null))
- throw new NullPointerException("Trying to add a collection with null element(s)");
- return super.addAll(index, c);
- }
-
- @Override
- public boolean containsAllKeys(final Collection<K> keys) {
- for (final K key : keys)
- if (!containsKey(key))
- return false;
- return true;
- }
-
- @Override
- public boolean containsKey(final K key) {
- return indexOfKey(key) >= 0;
- }
-
- @Nullable
- @Override
- public E get(final K key) {
- final int index = indexOfKey(key);
- return index >= 0 ? get(index) : null;
- }
-
- @Nullable
- @Override
- public E getLast(final K key) {
- final int index = lastIndexOfKey(key);
- return index >= 0 ? get(index) : null;
- }
-
- @Override
- public int indexOfKey(final K key) {
- final ListIterator<E> iterator = listIterator();
- while (iterator.hasNext()) {
- final int index = iterator.nextIndex();
- if (Objects.equals(iterator.next().getKey(), key))
- return index;
- }
- return -1;
- }
-
- @Override
- public int lastIndexOfKey(final K key) {
- final ListIterator<E> iterator = listIterator(size());
- while (iterator.hasPrevious()) {
- final int index = iterator.previousIndex();
- if (Objects.equals(iterator.previous().getKey(), key))
- return index;
- }
- return -1;
- }
-
- @Override
- public E set(final int index, @Nullable final E e) {
- if (e == null)
- throw new NullPointerException("Trying to set a null key");
- return super.set(index, e);
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java b/app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java
deleted file mode 100644
index be8ceb9b..00000000
--- a/app/src/main/java/com/wireguard/android/util/ObservableKeyedList.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import androidx.databinding.ObservableList;
-
-import com.wireguard.util.Keyed;
-import com.wireguard.util.KeyedList;
-
-/**
- * A list that is both keyed and observable.
- */
-
-public interface ObservableKeyedList<K, E extends Keyed<? extends K>>
- extends KeyedList<K, E>, ObservableList<E> {
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java b/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java
deleted file mode 100644
index 1d585856..00000000
--- a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedArrayList.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import androidx.annotation.Nullable;
-
-import com.wireguard.util.Keyed;
-import com.wireguard.util.SortedKeyedList;
-
-import java.util.AbstractList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import java.util.Spliterator;
-
-/**
- * KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
- * binary search to improve lookup and replacement times to O(log(n)). However, due to the
- * array-based nature of this class, insertion and removal of elements with anything but the largest
- * key still require O(n) time.
- */
-
-public class ObservableSortedKeyedArrayList<K, E extends Keyed<? extends K>>
- extends ObservableKeyedArrayList<K, E> implements ObservableSortedKeyedList<K, E> {
- @Nullable private final Comparator<? super K> comparator;
- private final transient KeyList<K, E> keyList = new KeyList<>(this);
-
- @SuppressWarnings("WeakerAccess")
- public ObservableSortedKeyedArrayList() {
- comparator = null;
- }
-
- public ObservableSortedKeyedArrayList(final Comparator<? super K> comparator) {
- this.comparator = comparator;
- }
-
- public ObservableSortedKeyedArrayList(final Collection<? extends E> c) {
- this();
- addAll(c);
- }
-
- public ObservableSortedKeyedArrayList(final SortedKeyedList<K, E> other) {
- this(other.comparator());
- addAll(other);
- }
-
- @Override
- public boolean add(final E e) {
- final int insertionPoint = getInsertionPoint(e);
- if (insertionPoint < 0) {
- // Skipping insertion is non-destructive if the new and existing objects are the same.
- if (e == get(-insertionPoint - 1))
- return false;
- throw new IllegalArgumentException("Element with same key already exists in list");
- }
- super.add(insertionPoint, e);
- return true;
- }
-
- @Override
- public void add(final int index, final E e) {
- final int insertionPoint = getInsertionPoint(e);
- if (insertionPoint < 0)
- throw new IllegalArgumentException("Element with same key already exists in list");
- if (insertionPoint != index)
- throw new IndexOutOfBoundsException("Wrong index given for element");
- super.add(index, e);
- }
-
- @Override
- public boolean addAll(final Collection<? extends E> c) {
- boolean didChange = false;
- for (final E e : c)
- if (add(e))
- didChange = true;
- return didChange;
- }
-
- @Override
- public boolean addAll(int index, final Collection<? extends E> c) {
- for (final E e : c)
- add(index++, e);
- return true;
- }
-
- @Nullable
- @Override
- public Comparator<? super K> comparator() {
- return comparator;
- }
-
- @Override
- public K firstKey() {
- if (isEmpty())
- // The parameter in the exception is only to shut
- // lint up, we never care for the exception message.
- throw new NoSuchElementException("Empty set");
- return get(0).getKey();
- }
-
- private int getInsertionPoint(final E e) {
- if (comparator != null) {
- return -Collections.binarySearch(keyList, e.getKey(), comparator) - 1;
- } else {
- @SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
- (List<Comparable<? super K>>) keyList;
- return -Collections.binarySearch(list, e.getKey()) - 1;
- }
- }
-
- @Override
- public int indexOfKey(final K key) {
- final int index;
- if (comparator != null) {
- index = Collections.binarySearch(keyList, key, comparator);
- } else {
- @SuppressWarnings("unchecked") final List<Comparable<? super K>> list =
- (List<Comparable<? super K>>) keyList;
- index = Collections.binarySearch(list, key);
- }
- return index >= 0 ? index : -1;
- }
-
- @Override
- public Set<K> keySet() {
- return keyList;
- }
-
- @Override
- public int lastIndexOfKey(final K key) {
- // There can never be more than one element with the same key in the list.
- return indexOfKey(key);
- }
-
- @Override
- public K lastKey() {
- if (isEmpty())
- // The parameter in the exception is only to shut
- // lint up, we never care for the exception message.
- throw new NoSuchElementException("Empty set");
- return get(size() - 1).getKey();
- }
-
- @Override
- public E set(final int index, final E e) {
- final int order;
- if (comparator != null) {
- order = comparator.compare(e.getKey(), get(index).getKey());
- } else {
- @SuppressWarnings("unchecked") final Comparable<? super K> key =
- (Comparable<? super K>) e.getKey();
- order = key.compareTo(get(index).getKey());
- }
- if (order != 0) {
- // Allow replacement if the new key would be inserted adjacent to the replaced element.
- final int insertionPoint = getInsertionPoint(e);
- if (insertionPoint < index || insertionPoint > index + 1)
- throw new IndexOutOfBoundsException("Wrong index given for element");
- }
- return super.set(index, e);
- }
-
- @Override
- public Collection<E> values() {
- return this;
- }
-
- private static final class KeyList<K, E extends Keyed<? extends K>>
- extends AbstractList<K> implements Set<K> {
- private final ObservableSortedKeyedArrayList<K, E> list;
-
- private KeyList(final ObservableSortedKeyedArrayList<K, E> list) {
- this.list = list;
- }
-
- @Override
- public K get(final int index) {
- return list.get(index).getKey();
- }
-
- @Override
- public int size() {
- return list.size();
- }
-
- @Override
- @SuppressWarnings("EmptyMethod")
- public Spliterator<K> spliterator() {
- return super.spliterator();
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java b/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java
deleted file mode 100644
index d796704e..00000000
--- a/app/src/main/java/com/wireguard/android/util/ObservableSortedKeyedList.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.util;
-
-import com.wireguard.util.Keyed;
-import com.wireguard.util.SortedKeyedList;
-
-/**
- * A list that is both sorted/keyed and observable.
- */
-
-public interface ObservableSortedKeyedList<K, E extends Keyed<? extends K>>
- extends ObservableKeyedList<K, E>, SortedKeyedList<K, E> {
-}
diff --git a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java
deleted file mode 100644
index bcfe14e3..00000000
--- a/app/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.viewmodel;
-
-import androidx.databinding.ObservableArrayList;
-import androidx.databinding.ObservableList;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Config;
-import com.wireguard.config.Peer;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-public class ConfigProxy implements Parcelable {
- public static final Parcelable.Creator<ConfigProxy> CREATOR = new ConfigProxyCreator();
-
- private final InterfaceProxy interfaze;
- private final ObservableList<PeerProxy> peers = new ObservableArrayList<>();
-
- private ConfigProxy(final Parcel in) {
- interfaze = in.readParcelable(InterfaceProxy.class.getClassLoader());
- in.readTypedList(peers, PeerProxy.CREATOR);
- for (final PeerProxy proxy : peers)
- proxy.bind(this);
- }
-
- public ConfigProxy(final Config other) {
- interfaze = new InterfaceProxy(other.getInterface());
- for (final Peer peer : other.getPeers()) {
- final PeerProxy proxy = new PeerProxy(peer);
- peers.add(proxy);
- proxy.bind(this);
- }
- }
-
- public ConfigProxy() {
- interfaze = new InterfaceProxy();
- }
-
- public PeerProxy addPeer() {
- final PeerProxy proxy = new PeerProxy();
- peers.add(proxy);
- proxy.bind(this);
- return proxy;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public InterfaceProxy getInterface() {
- return interfaze;
- }
-
- public ObservableList<PeerProxy> getPeers() {
- return peers;
- }
-
- public Config resolve() throws BadConfigException {
- final Collection<Peer> resolvedPeers = new ArrayList<>();
- for (final PeerProxy proxy : peers)
- resolvedPeers.add(proxy.resolve());
- return new Config.Builder()
- .setInterface(interfaze.resolve())
- .addPeers(resolvedPeers)
- .build();
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeParcelable(interfaze, flags);
- dest.writeTypedList(peers);
- }
-
- private static class ConfigProxyCreator implements Parcelable.Creator<ConfigProxy> {
- @Override
- public ConfigProxy createFromParcel(final Parcel in) {
- return new ConfigProxy(in);
- }
-
- @Override
- public ConfigProxy[] newArray(final int size) {
- return new ConfigProxy[size];
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java
deleted file mode 100644
index cc9f2dd8..00000000
--- a/app/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.viewmodel;
-
-import androidx.databinding.BaseObservable;
-import androidx.databinding.Bindable;
-import androidx.databinding.ObservableArrayList;
-import androidx.databinding.ObservableList;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.wireguard.android.BR;
-import com.wireguard.config.Attribute;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.Interface;
-import com.wireguard.crypto.Key;
-import com.wireguard.crypto.KeyFormatException;
-import com.wireguard.crypto.KeyPair;
-
-import java.net.InetAddress;
-import java.util.List;
-
-import java9.util.stream.Collectors;
-import java9.util.stream.StreamSupport;
-
-public class InterfaceProxy extends BaseObservable implements Parcelable {
- public static final Parcelable.Creator<InterfaceProxy> CREATOR = new InterfaceProxyCreator();
-
- private final ObservableList<String> excludedApplications = new ObservableArrayList<>();
- private String addresses;
- private String dnsServers;
- private String listenPort;
- private String mtu;
- private String privateKey;
- private String publicKey;
-
- private InterfaceProxy(final Parcel in) {
- addresses = in.readString();
- dnsServers = in.readString();
- in.readStringList(excludedApplications);
- listenPort = in.readString();
- mtu = in.readString();
- privateKey = in.readString();
- publicKey = in.readString();
- }
-
- public InterfaceProxy(final Interface other) {
- addresses = Attribute.join(other.getAddresses());
- final List<String> dnsServerStrings = StreamSupport.stream(other.getDnsServers())
- .map(InetAddress::getHostAddress)
- .collect(Collectors.toUnmodifiableList());
- dnsServers = Attribute.join(dnsServerStrings);
- excludedApplications.addAll(other.getExcludedApplications());
- listenPort = other.getListenPort().map(String::valueOf).orElse("");
- mtu = other.getMtu().map(String::valueOf).orElse("");
- final KeyPair keyPair = other.getKeyPair();
- privateKey = keyPair.getPrivateKey().toBase64();
- publicKey = keyPair.getPublicKey().toBase64();
- }
-
- public InterfaceProxy() {
- addresses = "";
- dnsServers = "";
- listenPort = "";
- mtu = "";
- privateKey = "";
- publicKey = "";
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public void generateKeyPair() {
- final KeyPair keyPair = new KeyPair();
- privateKey = keyPair.getPrivateKey().toBase64();
- publicKey = keyPair.getPublicKey().toBase64();
- notifyPropertyChanged(BR.privateKey);
- notifyPropertyChanged(BR.publicKey);
- }
-
- @Bindable
- public String getAddresses() {
- return addresses;
- }
-
- @Bindable
- public String getDnsServers() {
- return dnsServers;
- }
-
- public ObservableList<String> getExcludedApplications() {
- return excludedApplications;
- }
-
- @Bindable
- public String getListenPort() {
- return listenPort;
- }
-
- @Bindable
- public String getMtu() {
- return mtu;
- }
-
- @Bindable
- public String getPrivateKey() {
- return privateKey;
- }
-
- @Bindable
- public String getPublicKey() {
- return publicKey;
- }
-
- public Interface resolve() throws BadConfigException {
- final Interface.Builder builder = new Interface.Builder();
- if (!addresses.isEmpty())
- builder.parseAddresses(addresses);
- if (!dnsServers.isEmpty())
- builder.parseDnsServers(dnsServers);
- if (!excludedApplications.isEmpty())
- builder.excludeApplications(excludedApplications);
- if (!listenPort.isEmpty())
- builder.parseListenPort(listenPort);
- if (!mtu.isEmpty())
- builder.parseMtu(mtu);
- if (!privateKey.isEmpty())
- builder.parsePrivateKey(privateKey);
- return builder.build();
- }
-
- public void setAddresses(final String addresses) {
- this.addresses = addresses;
- notifyPropertyChanged(BR.addresses);
- }
-
- public void setDnsServers(final String dnsServers) {
- this.dnsServers = dnsServers;
- notifyPropertyChanged(BR.dnsServers);
- }
-
- public void setListenPort(final String listenPort) {
- this.listenPort = listenPort;
- notifyPropertyChanged(BR.listenPort);
- }
-
- public void setMtu(final String mtu) {
- this.mtu = mtu;
- notifyPropertyChanged(BR.mtu);
- }
-
- public void setPrivateKey(final String privateKey) {
- this.privateKey = privateKey;
- try {
- publicKey = new KeyPair(Key.fromBase64(privateKey)).getPublicKey().toBase64();
- } catch (final KeyFormatException ignored) {
- publicKey = "";
- }
- notifyPropertyChanged(BR.privateKey);
- notifyPropertyChanged(BR.publicKey);
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(addresses);
- dest.writeString(dnsServers);
- dest.writeStringList(excludedApplications);
- dest.writeString(listenPort);
- dest.writeString(mtu);
- dest.writeString(privateKey);
- dest.writeString(publicKey);
- }
-
- private static class InterfaceProxyCreator implements Parcelable.Creator<InterfaceProxy> {
- @Override
- public InterfaceProxy createFromParcel(final Parcel in) {
- return new InterfaceProxy(in);
- }
-
- @Override
- public InterfaceProxy[] newArray(final int size) {
- return new InterfaceProxy[size];
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java b/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java
deleted file mode 100644
index 7dc50f09..00000000
--- a/app/src/main/java/com/wireguard/android/viewmodel/PeerProxy.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.viewmodel;
-
-import androidx.databinding.BaseObservable;
-import androidx.databinding.Bindable;
-import androidx.databinding.Observable;
-import androidx.databinding.ObservableList;
-import android.os.Parcel;
-import android.os.Parcelable;
-import androidx.annotation.Nullable;
-
-import com.wireguard.android.BR;
-import com.wireguard.config.Attribute;
-import com.wireguard.config.BadConfigException;
-import com.wireguard.config.InetEndpoint;
-import com.wireguard.config.Peer;
-import com.wireguard.crypto.Key;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-import java9.util.Lists;
-import java9.util.Sets;
-import java9.util.stream.Collectors;
-import java9.util.stream.Stream;
-
-public class PeerProxy extends BaseObservable implements Parcelable {
- public static final Parcelable.Creator<PeerProxy> CREATOR = new PeerProxyCreator();
- private static final Set<String> IPV4_PUBLIC_NETWORKS = new LinkedHashSet<>(Lists.of(
- "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 static final Set<String> IPV4_WILDCARD = Sets.of("0.0.0.0/0");
-
- private final List<String> dnsRoutes = new ArrayList<>();
- private String allowedIps;
- private AllowedIpsState allowedIpsState = AllowedIpsState.INVALID;
- private String endpoint;
- @Nullable private InterfaceDnsListener interfaceDnsListener;
- @Nullable private ConfigProxy owner;
- @Nullable private PeerListListener peerListListener;
- private String persistentKeepalive;
- private String preSharedKey;
- private String publicKey;
- private int totalPeers;
-
- private PeerProxy(final Parcel in) {
- allowedIps = in.readString();
- endpoint = in.readString();
- persistentKeepalive = in.readString();
- preSharedKey = in.readString();
- publicKey = in.readString();
- }
-
- public PeerProxy(final Peer other) {
- allowedIps = Attribute.join(other.getAllowedIps());
- endpoint = other.getEndpoint().map(InetEndpoint::toString).orElse("");
- persistentKeepalive = other.getPersistentKeepalive().map(String::valueOf).orElse("");
- preSharedKey = other.getPreSharedKey().map(Key::toBase64).orElse("");
- publicKey = other.getPublicKey().toBase64();
- }
-
- public PeerProxy() {
- allowedIps = "";
- endpoint = "";
- persistentKeepalive = "";
- preSharedKey = "";
- publicKey = "";
- }
-
- public void bind(final ConfigProxy owner) {
- final InterfaceProxy interfaze = owner.getInterface();
- final ObservableList<PeerProxy> peers = owner.getPeers();
- if (interfaceDnsListener == null)
- interfaceDnsListener = new InterfaceDnsListener(this);
- interfaze.addOnPropertyChangedCallback(interfaceDnsListener);
- setInterfaceDns(interfaze.getDnsServers());
- if (peerListListener == null)
- peerListListener = new PeerListListener(this);
- peers.addOnListChangedCallback(peerListListener);
- setTotalPeers(peers.size());
- this.owner = owner;
- }
-
- private void calculateAllowedIpsState() {
- final AllowedIpsState newState;
- if (totalPeers == 1) {
- // String comparison works because we only care if allowedIps is a superset of one of
- // the above sets of (valid) *networks*. We are not checking for a superset based on
- // the individual addresses in each set.
- final Collection<String> networkStrings = getAllowedIpsSet();
- // If allowedIps contains both the wildcard and the public networks, then private
- // networks aren't excluded!
- if (networkStrings.containsAll(IPV4_WILDCARD))
- newState = AllowedIpsState.CONTAINS_IPV4_WILDCARD;
- else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
- newState = AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
- else
- newState = AllowedIpsState.OTHER;
- } else {
- newState = AllowedIpsState.INVALID;
- }
- if (newState != allowedIpsState) {
- allowedIpsState = newState;
- notifyPropertyChanged(BR.ableToExcludePrivateIps);
- notifyPropertyChanged(BR.excludingPrivateIps);
- }
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Bindable
- public String getAllowedIps() {
- return allowedIps;
- }
-
- private Set<String> getAllowedIpsSet() {
- return new LinkedHashSet<>(Lists.of(Attribute.split(allowedIps)));
- }
-
- @Bindable
- public String getEndpoint() {
- return endpoint;
- }
-
- @Bindable
- public String getPersistentKeepalive() {
- return persistentKeepalive;
- }
-
- @Bindable
- public String getPreSharedKey() {
- return preSharedKey;
- }
-
- @Bindable
- public String getPublicKey() {
- return publicKey;
- }
-
- @Bindable
- public boolean isAbleToExcludePrivateIps() {
- return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
- || allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD;
- }
-
- @Bindable
- public boolean isExcludingPrivateIps() {
- return allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS;
- }
-
- public Peer resolve() throws BadConfigException {
- final Peer.Builder builder = new Peer.Builder();
- if (!allowedIps.isEmpty())
- builder.parseAllowedIPs(allowedIps);
- if (!endpoint.isEmpty())
- builder.parseEndpoint(endpoint);
- if (!persistentKeepalive.isEmpty())
- builder.parsePersistentKeepalive(persistentKeepalive);
- if (!preSharedKey.isEmpty())
- builder.parsePreSharedKey(preSharedKey);
- if (!publicKey.isEmpty())
- builder.parsePublicKey(publicKey);
- return builder.build();
- }
-
- public void setAllowedIps(final String allowedIps) {
- this.allowedIps = allowedIps;
- notifyPropertyChanged(BR.allowedIps);
- calculateAllowedIpsState();
- }
-
- public void setEndpoint(final String endpoint) {
- this.endpoint = endpoint;
- notifyPropertyChanged(BR.endpoint);
- }
-
- public void setExcludingPrivateIps(final boolean excludingPrivateIps) {
- if (!isAbleToExcludePrivateIps() || isExcludingPrivateIps() == excludingPrivateIps)
- return;
- final Set<String> oldNetworks = excludingPrivateIps ? IPV4_WILDCARD : IPV4_PUBLIC_NETWORKS;
- final Set<String> newNetworks = excludingPrivateIps ? IPV4_PUBLIC_NETWORKS : IPV4_WILDCARD;
- final Collection<String> input = getAllowedIpsSet();
- final int outputSize = input.size() - oldNetworks.size() + newNetworks.size();
- final Collection<String> output = new LinkedHashSet<>(outputSize);
- boolean replaced = false;
- // Replace the first instance of the wildcard with the public network list, or vice versa.
- for (final String network : input) {
- if (oldNetworks.contains(network)) {
- if (!replaced) {
- for (final String replacement : newNetworks)
- if (!output.contains(replacement))
- output.add(replacement);
- replaced = true;
- }
- } else if (!output.contains(network)) {
- output.add(network);
- }
- }
- // DNS servers only need to handled specially when we're excluding private IPs.
- if (excludingPrivateIps)
- output.addAll(dnsRoutes);
- else
- output.removeAll(dnsRoutes);
- allowedIps = Attribute.join(output);
- allowedIpsState = excludingPrivateIps ?
- AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS : AllowedIpsState.CONTAINS_IPV4_WILDCARD;
- notifyPropertyChanged(BR.allowedIps);
- notifyPropertyChanged(BR.excludingPrivateIps);
- }
-
- private void setInterfaceDns(final CharSequence dnsServers) {
- final List<String> newDnsRoutes = Stream.of(Attribute.split(dnsServers))
- .filter(server -> !server.contains(":"))
- .map(server -> server + "/32")
- .collect(Collectors.toUnmodifiableList());
- if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
- final Collection<String> input = getAllowedIpsSet();
- final Collection<String> output = new LinkedHashSet<>(input.size() + 1);
- // Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
- for (final String network : input)
- if (!dnsRoutes.contains(network) || newDnsRoutes.contains(network))
- output.add(network);
- // Since output is a Set, this does the Right Thing™ (it does not duplicate networks).
- output.addAll(newDnsRoutes);
- // None of the public networks are /32s, so this cannot change the AllowedIPs state.
- allowedIps = Attribute.join(output);
- notifyPropertyChanged(BR.allowedIps);
- }
- dnsRoutes.clear();
- dnsRoutes.addAll(newDnsRoutes);
- }
-
- public void setPersistentKeepalive(final String persistentKeepalive) {
- this.persistentKeepalive = persistentKeepalive;
- notifyPropertyChanged(BR.persistentKeepalive);
- }
-
- public void setPreSharedKey(final String preSharedKey) {
- this.preSharedKey = preSharedKey;
- notifyPropertyChanged(BR.preSharedKey);
- }
-
- public void setPublicKey(final String publicKey) {
- this.publicKey = publicKey;
- notifyPropertyChanged(BR.publicKey);
- }
-
- private void setTotalPeers(final int totalPeers) {
- if (this.totalPeers == totalPeers)
- return;
- this.totalPeers = totalPeers;
- calculateAllowedIpsState();
- }
-
- public void unbind() {
- if (owner == null)
- return;
- final InterfaceProxy interfaze = owner.getInterface();
- final ObservableList<PeerProxy> peers = owner.getPeers();
- if (interfaceDnsListener != null)
- interfaze.removeOnPropertyChangedCallback(interfaceDnsListener);
- if (peerListListener != null)
- peers.removeOnListChangedCallback(peerListListener);
- peers.remove(this);
- setInterfaceDns("");
- setTotalPeers(0);
- owner = null;
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeString(allowedIps);
- dest.writeString(endpoint);
- dest.writeString(persistentKeepalive);
- dest.writeString(preSharedKey);
- dest.writeString(publicKey);
- }
-
- private enum AllowedIpsState {
- CONTAINS_IPV4_PUBLIC_NETWORKS,
- CONTAINS_IPV4_WILDCARD,
- INVALID,
- OTHER
- }
-
- private static final class InterfaceDnsListener extends Observable.OnPropertyChangedCallback {
- private final WeakReference<PeerProxy> weakPeerProxy;
-
- private InterfaceDnsListener(final PeerProxy peerProxy) {
- weakPeerProxy = new WeakReference<>(peerProxy);
- }
-
- @Override
- public void onPropertyChanged(final Observable sender, final int propertyId) {
- @Nullable final PeerProxy peerProxy = weakPeerProxy.get();
- if (peerProxy == null) {
- sender.removeOnPropertyChangedCallback(this);
- return;
- }
- // This shouldn't be possible, but try to avoid a ClassCastException anyway.
- if (!(sender instanceof InterfaceProxy))
- return;
- if (!(propertyId == BR._all || propertyId == BR.dnsServers))
- return;
- peerProxy.setInterfaceDns(((InterfaceProxy) sender).getDnsServers());
- }
- }
-
- private static final class PeerListListener
- extends ObservableList.OnListChangedCallback<ObservableList<PeerProxy>> {
- private final WeakReference<PeerProxy> weakPeerProxy;
-
- private PeerListListener(final PeerProxy peerProxy) {
- weakPeerProxy = new WeakReference<>(peerProxy);
- }
-
- @Override
- public void onChanged(final ObservableList<PeerProxy> sender) {
- @Nullable final PeerProxy peerProxy = weakPeerProxy.get();
- if (peerProxy == null) {
- sender.removeOnListChangedCallback(this);
- return;
- }
- peerProxy.setTotalPeers(sender.size());
- }
-
- @Override
- public void onItemRangeChanged(final ObservableList<PeerProxy> sender,
- final int positionStart, final int itemCount) {
- // Do nothing.
- }
-
- @Override
- public void onItemRangeInserted(final ObservableList<PeerProxy> sender,
- final int positionStart, final int itemCount) {
- onChanged(sender);
- }
-
- @Override
- public void onItemRangeMoved(final ObservableList<PeerProxy> sender,
- final int fromPosition, final int toPosition,
- final int itemCount) {
- // Do nothing.
- }
-
- @Override
- public void onItemRangeRemoved(final ObservableList<PeerProxy> sender,
- final int positionStart, final int itemCount) {
- onChanged(sender);
- }
- }
-
- private static class PeerProxyCreator implements Parcelable.Creator<PeerProxy> {
- @Override
- public PeerProxy createFromParcel(final Parcel in) {
- return new PeerProxy(in);
- }
-
- @Override
- public PeerProxy[] newArray(final int size) {
- return new PeerProxy[size];
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java b/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java
deleted file mode 100644
index 79572aa3..00000000
--- a/app/src/main/java/com/wireguard/android/widget/KeyInputFilter.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget;
-
-import androidx.annotation.Nullable;
-import android.text.InputFilter;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-
-import com.wireguard.crypto.Key;
-
-/**
- * InputFilter for entering WireGuard private/public keys encoded with base64.
- */
-
-public class KeyInputFilter implements InputFilter {
- private static boolean isAllowed(final char c) {
- return Character.isLetterOrDigit(c) || c == '+' || c == '/';
- }
-
- public static InputFilter newInstance() {
- return new KeyInputFilter();
- }
-
- @Nullable
- @Override
- public CharSequence filter(final CharSequence source,
- final int sStart, final int sEnd,
- final Spanned dest,
- final int dStart, final int dEnd) {
- SpannableStringBuilder replacement = null;
- int rIndex = 0;
- final int dLength = dest.length();
- for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
- final char c = source.charAt(sIndex);
- final int dIndex = dStart + (sIndex - sStart);
- // 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.getLength() && isAllowed(c)) ||
- (dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
- dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
- ++rIndex;
- } else {
- if (replacement == null)
- replacement = new SpannableStringBuilder(source, sStart, sEnd);
- replacement.delete(rIndex, rIndex + 1);
- }
- }
- return replacement;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java b/app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java
deleted file mode 100644
index 2fe9c924..00000000
--- a/app/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright © 2017-2019 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.widget.RelativeLayout;
-
-import com.wireguard.android.R;
-
-public class MultiselectableRelativeLayout extends RelativeLayout {
- private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
- private boolean multiselected;
-
- public MultiselectableRelativeLayout(final Context context) {
- super(context);
- }
-
- public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected int[] onCreateDrawableState(final int extraSpace) {
- if (multiselected) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
- mergeDrawableStates(drawableState, STATE_MULTISELECTED);
- return drawableState;
- }
- return super.onCreateDrawableState(extraSpace);
- }
-
- public void setMultiSelected(final boolean on) {
- if (!multiselected) {
- multiselected = true;
- refreshDrawableState();
- }
- setActivated(on);
- }
-
- public void setSingleSelected(final boolean on) {
- if (multiselected) {
- multiselected = false;
- refreshDrawableState();
- }
- setActivated(on);
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java b/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java
deleted file mode 100644
index 07759a18..00000000
--- a/app/src/main/java/com/wireguard/android/widget/NameInputFilter.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget;
-
-import androidx.annotation.Nullable;
-import android.text.InputFilter;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-
-import com.wireguard.android.model.Tunnel;
-
-/**
- * InputFilter for entering WireGuard configuration names (Linux interface names).
- */
-
-public class NameInputFilter implements InputFilter {
- private static boolean isAllowed(final char c) {
- return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
- }
-
- public static InputFilter newInstance() {
- return new NameInputFilter();
- }
-
- @Nullable
- @Override
- public CharSequence filter(final CharSequence source,
- final int sStart, final int sEnd,
- final Spanned dest,
- final int dStart, final int dEnd) {
- SpannableStringBuilder replacement = null;
- int rIndex = 0;
- final int dLength = dest.length();
- for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
- final char c = source.charAt(sIndex);
- final int dIndex = dStart + (sIndex - sStart);
- // 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) {
- ++rIndex;
- } else {
- if (replacement == null)
- replacement = new SpannableStringBuilder(source, sStart, sEnd);
- replacement.delete(rIndex, rIndex + 1);
- }
- }
- return replacement;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java b/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java
deleted file mode 100644
index 61ce1362..00000000
--- a/app/src/main/java/com/wireguard/android/widget/SlashDrawable.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright © 2018 The Android Open Source Project
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget;
-
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.Path.Direction;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Region;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import androidx.annotation.ColorInt;
-import androidx.annotation.IntRange;
-import androidx.annotation.Nullable;
-import android.util.FloatProperty;
-
-@TargetApi(Build.VERSION_CODES.N)
-public class SlashDrawable extends Drawable {
-
- private static final float CENTER_X = 10.65f;
- private static final float CENTER_Y = 11.869239f;
- private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
- // Draw the slash washington-monument style; rotate to no-u-turn style
- private static final float DEFAULT_ROTATION = -45f;
- private static final long QS_ANIM_LENGTH = 350;
- private static final float SCALE = 24f;
- private static final float SLASH_HEIGHT = 28f;
- // These values are derived in un-rotated (vertical) orientation
- private static final float SLASH_WIDTH = 1.8384776f;
- // Bottom is derived during animation
- private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
- private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
- private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
- private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
- @Override
- public Float get(final SlashDrawable object) {
- return object.mCurrentSlashLength;
- }
-
- @Override
- public void setValue(final SlashDrawable object, final float value) {
- object.mCurrentSlashLength = value;
- }
- };
- private final Drawable mDrawable;
- private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private final Path mPath = new Path();
- private final RectF mSlashRect = new RectF(0, 0, 0, 0);
- private boolean mAnimationEnabled = true;
- // Animate this value on change
- private float mCurrentSlashLength;
- private float mRotation;
- private boolean mSlashed;
-
- public SlashDrawable(final Drawable d) {
- mDrawable = d;
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void draw(final Canvas canvas) {
- canvas.save();
- final Matrix m = new Matrix();
- final int width = getBounds().width();
- final int height = getBounds().height();
- final float radiusX = scale(CORNER_RADIUS, width);
- final float radiusY = scale(CORNER_RADIUS, height);
- updateRect(
- scale(LEFT, width),
- scale(TOP, height),
- scale(RIGHT, width),
- scale(TOP + mCurrentSlashLength, height)
- );
-
- mPath.reset();
- // Draw the slash vertically
- mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
- // Rotate -45 + desired rotation
- m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
- mPath.transform(m);
- canvas.drawPath(mPath, mPaint);
-
- // Rotate back to vertical
- m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
- mPath.transform(m);
-
- // Draw another rect right next to the first, for clipping
- m.setTranslate(mSlashRect.width(), 0);
- mPath.transform(m);
- mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
- m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
- mPath.transform(m);
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
- canvas.clipPath(mPath, Region.Op.DIFFERENCE);
- else
- canvas.clipOutPath(mPath);
-
- mDrawable.draw(canvas);
- canvas.restore();
- }
-
- @Override
- public int getIntrinsicHeight() {
- return mDrawable.getIntrinsicHeight();
- }
-
- @Override
- public int getIntrinsicWidth() {
- return mDrawable.getIntrinsicWidth();
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.OPAQUE;
- }
-
- @Override
- protected void onBoundsChange(final Rect bounds) {
- super.onBoundsChange(bounds);
- mDrawable.setBounds(bounds);
- }
-
- private float scale(final float frac, final int width) {
- return frac * width;
- }
-
- @Override
- public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
- mDrawable.setAlpha(alpha);
- mPaint.setAlpha(alpha);
- }
-
- public void setAnimationEnabled(final boolean enabled) {
- mAnimationEnabled = enabled;
- }
-
- @Override
- public void setColorFilter(@Nullable final ColorFilter colorFilter) {
- mDrawable.setColorFilter(colorFilter);
- mPaint.setColorFilter(colorFilter);
- }
-
- private void setDrawableTintList(@Nullable final ColorStateList tint) {
- mDrawable.setTintList(tint);
- }
-
- public void setRotation(final float rotation) {
- if (mRotation == rotation)
- return;
- mRotation = rotation;
- invalidateSelf();
- }
-
- @SuppressWarnings("unchecked")
- public void setSlashed(final boolean slashed) {
- if (mSlashed == slashed) return;
-
- mSlashed = slashed;
-
- final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
- final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
-
- if (mAnimationEnabled) {
- final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
- anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
- anim.setDuration(QS_ANIM_LENGTH);
- anim.start();
- } else {
- mCurrentSlashLength = end;
- invalidateSelf();
- }
- }
-
- @Override
- public void setTint(@ColorInt final int tintColor) {
- super.setTint(tintColor);
- mDrawable.setTint(tintColor);
- mPaint.setColor(tintColor);
- }
-
- @Override
- public void setTintList(@Nullable final ColorStateList tint) {
- super.setTintList(tint);
- setDrawableTintList(tint);
- mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
- invalidateSelf();
- }
-
- @Override
- public void setTintMode(final Mode tintMode) {
- super.setTintMode(tintMode);
- mDrawable.setTintMode(tintMode);
- }
-
- private void updateRect(final float left, final float top, final float right, final float bottom) {
- mSlashRect.left = left;
- mSlashRect.top = top;
- mSlashRect.right = right;
- mSlashRect.bottom = bottom;
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java b/app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java
deleted file mode 100644
index dcb9aceb..00000000
--- a/app/src/main/java/com/wireguard/android/widget/ToggleSwitch.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright © 2013 The Android Open Source Project
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget;
-
-import android.content.Context;
-import android.os.Parcelable;
-import androidx.annotation.Nullable;
-import android.util.AttributeSet;
-import android.widget.Switch;
-
-public class ToggleSwitch extends Switch {
- private boolean isRestoringState;
- @Nullable private OnBeforeCheckedChangeListener listener;
-
- public ToggleSwitch(final Context context) {
- this(context, null);
- }
-
- @SuppressWarnings({"SameParameterValue", "WeakerAccess"})
- public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void onRestoreInstanceState(final Parcelable state) {
- isRestoringState = true;
- super.onRestoreInstanceState(state);
- isRestoringState = false;
- }
-
- @Override
- public void setChecked(final boolean checked) {
- if (checked == isChecked())
- return;
- if (isRestoringState || listener == null) {
- super.setChecked(checked);
- return;
- }
- setEnabled(false);
- listener.onBeforeCheckedChanged(this, checked);
- }
-
- public void setCheckedInternal(final boolean checked) {
- super.setChecked(checked);
- setEnabled(true);
- }
-
- public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
- this.listener = listener;
- }
-
- public interface OnBeforeCheckedChangeListener {
- void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java
deleted file mode 100644
index 616e176e..00000000
--- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButtonBehavior.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget.fab;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
-import android.util.AttributeSet;
-import android.view.View;
-
-public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
-
- private static final long ANIMATION_DURATION = 250;
- private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
-
- public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
- final float origin = child.getBehaviorYTranslation();
- if (Math.abs(destination - origin) < fullSpan / 2) {
- child.setBehaviorYTranslation(destination);
- return;
- }
- final ValueAnimator animator = new ValueAnimator();
- animator.setFloatValues(origin, destination);
- animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration((long) (ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(final Animator a) {
- child.setBehaviorYTranslation(destination);
- }
- });
- animator.addUpdateListener(a -> child.setBehaviorYTranslation((float) a.getAnimatedValue()));
- animator.start();
- }
-
- @Override
- public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
- final View dependency) {
- return dependency instanceof Snackbar.SnackbarLayout;
- }
-
- @Override
- public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
- final View dependency) {
- animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
- return true;
- }
-
- @Override
- public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
- final View dependency) {
- animateChange(child, 0, dependency.getMeasuredHeight());
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java
deleted file mode 100644
index 9704081b..00000000
--- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java
+++ /dev/null
@@ -1,629 +0,0 @@
-/*
- * Copyright © 2014 Jerzy Chalupski
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget.fab;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import androidx.annotation.Keep;
-import androidx.annotation.Nullable;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import androidx.core.content.res.ResourcesCompat;
-import androidx.appcompat.widget.AppCompatTextView;
-import android.util.AttributeSet;
-import android.view.ContextThemeWrapper;
-import android.view.TouchDelegate;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.OvershootInterpolator;
-import android.widget.TextView;
-
-import com.wireguard.android.R;
-
-public class FloatingActionsMenu extends ViewGroup {
- public static final int EXPAND_DOWN = 1;
- public static final int EXPAND_LEFT = 2;
- public static final int EXPAND_RIGHT = 3;
- public static final int EXPAND_UP = 0;
- public static final int LABELS_ON_LEFT_SIDE = 0;
- public static final int LABELS_ON_RIGHT_SIDE = 1;
- private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
- private static final int ANIMATION_DURATION = 300;
- private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equals("ASUS");
- private static final float COLLAPSED_PLUS_ROTATION = 0f;
- private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
- private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
- private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
- private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
- private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
- private final Rect touchArea = new Rect(0, 0, 0, 0);
- private float behaviorYTranslation;
- @Nullable private FloatingActionButton mAddButton;
- private int mButtonSpacing;
- private int mButtonsCount;
- private int mExpandDirection;
- private boolean mExpanded;
- private int mLabelsMargin;
- private int mLabelsPosition;
- private int mLabelsStyle;
- private int mLabelsVerticalOffset;
- @Nullable private OnFloatingActionsMenuUpdateListener mListener;
- private int mMaxButtonHeight;
- private int mMaxButtonWidth;
- @Nullable private RotatingDrawable mRotatingDrawable;
- @Nullable private TouchDelegateGroup mTouchDelegateGroup;
- private float scrollYTranslation;
-
- public FloatingActionsMenu(final Context context) {
- this(context, null);
- }
-
- public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs);
- }
-
- public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
- super(context, attrs, defStyle);
- init(context, attrs);
- }
-
- private static int adjustForOvershoot(final int dimension) {
- return dimension * 12 / 10;
- }
-
- public void addButton(final LabeledFloatingActionButton button) {
- addView(button, mButtonsCount - 1);
- mButtonsCount++;
-
- if (mLabelsStyle != 0) {
- createLabels();
- }
- }
-
- public void collapse() {
- collapse(false);
- }
-
- private void collapse(final boolean immediately) {
- if (mExpanded) {
- mExpanded = false;
- mTouchDelegateGroup.setEnabled(false);
- mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
- mCollapseAnimation.start();
- mExpandAnimation.cancel();
-
- if (mListener != null) {
- mListener.onMenuCollapsed();
- }
- }
- }
-
- public void collapseImmediately() {
- collapse(true);
- }
-
- private void createAddButton(final Context context) {
- final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
- mRotatingDrawable = rotatingDrawable;
-
- final TimeInterpolator interpolator = new OvershootInterpolator();
-
- final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
- final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
-
- collapseAnimator.setInterpolator(interpolator);
- expandAnimator.setInterpolator(interpolator);
-
- mExpandAnimation.play(expandAnimator);
- mCollapseAnimation.play(collapseAnimator);
-
- mAddButton = new FloatingActionButton(context);
- mAddButton.setImageDrawable(rotatingDrawable);
- mAddButton.setId(R.id.fab_expand_menu_button);
- mAddButton.setOnClickListener(v -> toggle());
-
- addView(mAddButton, super.generateDefaultLayoutParams());
- mButtonsCount++;
- }
-
- private void createLabels() {
- final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
-
- for (int i = 0; i < mButtonsCount; i++) {
- final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
-
- if (button instanceof LabeledFloatingActionButton) {
- final String title = ((LabeledFloatingActionButton) button).getTitle();
-
- final AppCompatTextView label = new AppCompatTextView(context);
- if (!BROKEN_LABEL_STYLE)
- label.setTextAppearance(context, mLabelsStyle);
- label.setText(title);
- addView(label);
-
- button.setTag(R.id.fab_label, label);
- }
- }
- }
-
- public void expand() {
- if (!mExpanded) {
- mExpanded = true;
- mTouchDelegateGroup.setEnabled(true);
- mCollapseAnimation.cancel();
- mExpandAnimation.start();
-
- if (mListener != null) {
- mListener.onMenuExpanded();
- }
- }
- }
-
- private boolean expandsHorizontally() {
- return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
- }
-
- @Override
- protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(super.generateDefaultLayoutParams());
- }
-
- @Override
- public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
- return new LayoutParams(super.generateLayoutParams(attrs));
- }
-
- @Override
- protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
- return new LayoutParams(super.generateLayoutParams(p));
- }
-
- public float getBehaviorYTranslation() {
- return behaviorYTranslation;
- }
-
- public float getScrollYTranslation() {
- return scrollYTranslation;
- }
-
- private void init(final Context context, @Nullable final AttributeSet attributeSet) {
- mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
- mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
- mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
-
- mTouchDelegateGroup = new TouchDelegateGroup(this);
- setTouchDelegate(mTouchDelegateGroup);
-
- final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
- mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
- mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
- mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
- attr.recycle();
-
- if (mLabelsStyle != 0 && expandsHorizontally()) {
- throw new IllegalStateException("Action labels in horizontal expand orientation are not supported");
- }
-
- createAddButton(context);
- }
-
- public boolean isExpanded() {
- return mExpanded;
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- bringChildToFront(mAddButton);
- mButtonsCount = getChildCount();
-
- if (mLabelsStyle != 0) {
- createLabels();
- }
- }
-
- @Override
- protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
- switch (mExpandDirection) {
- case EXPAND_UP:
- case EXPAND_DOWN:
- final boolean expandUp = mExpandDirection == EXPAND_UP;
-
- if (changed) {
- mTouchDelegateGroup.clearTouchDelegates();
- }
-
- final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
- // Ensure mAddButton is centered on the line where the buttons should be
- final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
- ? r - l - mMaxButtonWidth / 2
- : mMaxButtonWidth / 2);
- final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
- mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
-
- final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
- final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
- ? buttonsHorizontalCenter - labelsOffset
- : buttonsHorizontalCenter + labelsOffset;
-
- int nextY = expandUp ?
- addButtonY - mButtonSpacing :
- addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
-
- for (int i = mButtonsCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
-
- if (child == mAddButton || child.getVisibility() == GONE) continue;
-
- final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
- final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
- child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
-
- final float collapsedTranslation = addButtonY - childY;
- final float expandedTranslation = 0f;
-
- child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
- child.setAlpha(mExpanded ? 1f : 0f);
-
- final LayoutParams params = (LayoutParams) child.getLayoutParams();
- params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
- params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
- params.setAnimationsTarget(child);
-
- final View label = (View) child.getTag(R.id.fab_label);
- if (label != null) {
- final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
- ? labelsXNearButton - label.getMeasuredWidth()
- : labelsXNearButton + label.getMeasuredWidth();
-
- final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
- ? labelXAwayFromButton
- : labelsXNearButton;
-
- final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
- ? labelsXNearButton
- : labelXAwayFromButton;
-
- final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
-
- label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
-
- touchArea.set(Math.min(childX, labelLeft),
- childY - mButtonSpacing / 2,
- Math.max(childX + child.getMeasuredWidth(), labelRight),
- childY + child.getMeasuredHeight() + mButtonSpacing / 2);
- mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
-
- label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
- label.setAlpha(mExpanded ? 1f : 0f);
-
- final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
- labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
- labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
- labelParams.setAnimationsTarget(label);
- }
-
- nextY = expandUp ?
- childY - mButtonSpacing :
- childY + child.getMeasuredHeight() + mButtonSpacing;
- }
- break;
-
- case EXPAND_LEFT:
- case EXPAND_RIGHT:
- final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
-
- final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
- // Ensure mAddButton is centered on the line where the buttons should be
- final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
- mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
-
- int nextX = expandLeft ?
- addButtonX - mButtonSpacing :
- addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
-
- for (int i = mButtonsCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
-
- if (child == mAddButton || child.getVisibility() == GONE) continue;
-
- final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
- final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
- child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
-
- final float collapsedTranslation = addButtonX - childX;
- final float expandedTranslation = 0f;
-
- child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
- child.setAlpha(mExpanded ? 1f : 0f);
-
- final LayoutParams params = (LayoutParams) child.getLayoutParams();
- params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
- params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
- params.setAnimationsTarget(child);
-
- nextX = expandLeft ?
- childX - mButtonSpacing :
- childX + child.getMeasuredWidth() + mButtonSpacing;
- }
-
- break;
- }
- }
-
- @Override
- protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- measureChildren(widthMeasureSpec, heightMeasureSpec);
-
- int width = 0;
- int height = 0;
-
- mMaxButtonWidth = 0;
- mMaxButtonHeight = 0;
- int maxLabelWidth = 0;
-
- for (int i = 0; i < mButtonsCount; i++) {
- final View child = getChildAt(i);
-
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- switch (mExpandDirection) {
- case EXPAND_UP:
- case EXPAND_DOWN:
- mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
- height += child.getMeasuredHeight();
- break;
- case EXPAND_LEFT:
- case EXPAND_RIGHT:
- width += child.getMeasuredWidth();
- mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
- break;
- }
-
- if (!expandsHorizontally()) {
- final TextView label = (TextView) child.getTag(R.id.fab_label);
- if (label != null) {
- maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
- }
- }
- }
-
- if (expandsHorizontally()) {
- height = mMaxButtonHeight;
- } else {
- width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
- }
-
- switch (mExpandDirection) {
- case EXPAND_UP:
- case EXPAND_DOWN:
- height += mButtonSpacing * (mButtonsCount - 1);
- height = adjustForOvershoot(height);
- break;
- case EXPAND_LEFT:
- case EXPAND_RIGHT:
- width += mButtonSpacing * (mButtonsCount - 1);
- width = adjustForOvershoot(width);
- break;
- }
-
- setMeasuredDimension(width, height);
- }
-
- @Override
- public void onRestoreInstanceState(final Parcelable state) {
- if (state instanceof SavedState) {
- final SavedState savedState = (SavedState) state;
- mExpanded = savedState.mExpanded;
- mTouchDelegateGroup.setEnabled(mExpanded);
-
- if (mRotatingDrawable != null) {
- mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
- }
-
- super.onRestoreInstanceState(savedState.getSuperState());
- } else {
- super.onRestoreInstanceState(state);
- }
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- final Parcelable superState = super.onSaveInstanceState();
- final SavedState savedState = new SavedState(superState);
- savedState.mExpanded = mExpanded;
-
- return savedState;
- }
-
- public void removeButton(final LabeledFloatingActionButton button) {
- removeView(button.getLabelView());
- removeView(button);
- button.setTag(R.id.fab_label, null);
- mButtonsCount--;
- }
-
- public void setBehaviorYTranslation(final float behaviorYTranslation) {
- this.behaviorYTranslation = behaviorYTranslation;
- setTranslationY(behaviorYTranslation + scrollYTranslation);
- }
-
- @Override
- public void setEnabled(final boolean enabled) {
- super.setEnabled(enabled);
-
- mAddButton.setEnabled(enabled);
- }
-
- public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
- mListener = listener;
- }
-
- public void setScrollYTranslation(final float scrollYTranslation) {
- this.scrollYTranslation = scrollYTranslation;
- setTranslationY(behaviorYTranslation + scrollYTranslation);
- }
-
- public void toggle() {
- if (mExpanded) {
- collapse();
- } else {
- expand();
- }
- }
-
- public interface OnFloatingActionsMenuUpdateListener {
- void onMenuCollapsed();
-
- void onMenuExpanded();
- }
-
- private static class RotatingDrawable extends LayerDrawable {
- private float mRotation;
-
- RotatingDrawable(final Drawable drawable) {
- super(new Drawable[]{drawable});
- }
-
- @Override
- public void draw(final Canvas canvas) {
- canvas.save();
- canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
- super.draw(canvas);
- canvas.restore();
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public float getRotation() {
- return mRotation;
- }
-
- @Keep
- @SuppressWarnings("UnusedDeclaration")
- public void setRotation(final float rotation) {
- mRotation = rotation;
- invalidateSelf();
- }
- }
-
- public static class SavedState extends BaseSavedState {
- public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
-
- @Override
- public SavedState createFromParcel(final Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(final int size) {
- return new SavedState[size];
- }
- };
- private boolean mExpanded;
-
- public SavedState(final Parcelable parcel) {
- super(parcel);
- }
-
- private SavedState(final Parcel in) {
- super(in);
- mExpanded = in.readInt() == 1;
- }
-
- @Override
- public void writeToParcel(final Parcel out, final int flags) {
- super.writeToParcel(out, flags);
- out.writeInt(mExpanded ? 1 : 0);
- }
- }
-
- private class LayoutParams extends ViewGroup.LayoutParams {
-
- private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
- private final ObjectAnimator mCollapseDir = new ObjectAnimator();
- private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
- private final ObjectAnimator mExpandDir = new ObjectAnimator();
- private boolean animationsSetToPlay;
-
- LayoutParams(final ViewGroup.LayoutParams source) {
- super(source);
-
- mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
- mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
- mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
- mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
-
- mCollapseAlpha.setProperty(View.ALPHA);
- mCollapseAlpha.setFloatValues(1f, 0f);
-
- mExpandAlpha.setProperty(View.ALPHA);
- mExpandAlpha.setFloatValues(0f, 1f);
-
- switch (mExpandDirection) {
- case EXPAND_UP:
- case EXPAND_DOWN:
- mCollapseDir.setProperty(View.TRANSLATION_Y);
- mExpandDir.setProperty(View.TRANSLATION_Y);
- break;
- case EXPAND_LEFT:
- case EXPAND_RIGHT:
- mCollapseDir.setProperty(View.TRANSLATION_X);
- mExpandDir.setProperty(View.TRANSLATION_X);
- break;
- }
- }
-
- private void addLayerTypeListener(final Animator animator, final View view) {
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(final Animator animation) {
- view.setLayerType(LAYER_TYPE_NONE, null);
- }
-
- @Override
- public void onAnimationStart(final Animator animation) {
- view.setLayerType(LAYER_TYPE_HARDWARE, null);
- }
- });
- }
-
- public void setAnimationsTarget(final View view) {
- mCollapseAlpha.setTarget(view);
- mCollapseDir.setTarget(view);
- mExpandAlpha.setTarget(view);
- mExpandDir.setTarget(view);
-
- // Now that the animations have targets, set them to be played
- if (!animationsSetToPlay) {
- addLayerTypeListener(mExpandDir, view);
- addLayerTypeListener(mCollapseDir, view);
-
- mCollapseAnimation.play(mCollapseAlpha);
- mCollapseAnimation.play(mCollapseDir);
- mExpandAnimation.play(mExpandAlpha);
- mExpandAnimation.play(mExpandDir);
- animationsSetToPlay = true;
- }
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java
deleted file mode 100644
index e1af4484..00000000
--- a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenuRecyclerViewScrollListener.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget.fab;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
- private static final float SCALE_FACTOR = 1.5f;
- private final FloatingActionsMenu menu;
-
- public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
- this.menu = menu;
- }
-
- private static float bound(final float min, final float proposal, final float max) {
- return Math.min(max, Math.max(min, proposal));
- }
-
- @Override
- public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
- super.onScrolled(recyclerView, dx, dy);
- menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
- }
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java b/app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java
deleted file mode 100644
index be94a6e5..00000000
--- a/app/src/main/java/com/wireguard/android/widget/fab/LabeledFloatingActionButton.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright © 2014 Jerzy Chalupski
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget.fab;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import androidx.annotation.Nullable;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.wireguard.android.R;
-
-public class LabeledFloatingActionButton extends FloatingActionButton {
-
- @Nullable private final String title;
-
- public LabeledFloatingActionButton(final Context context) {
- this(context, null);
- }
-
- public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
- super(context, attrs, defStyle);
-
- final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
- title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
- attr.recycle();
- }
-
- @Nullable
- TextView getLabelView() {
- return (TextView) getTag(R.id.fab_label);
- }
-
- @Nullable
- public String getTitle() {
- return title;
- }
-
- @Override
- public void setVisibility(final int visibility) {
- final TextView label = getLabelView();
- if (label != null) {
- label.setVisibility(visibility);
- }
-
- super.setVisibility(visibility);
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java b/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java
deleted file mode 100644
index e16d1d3e..00000000
--- a/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright © 2014 Jerzy Chalupski
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.android.widget.fab;
-
-import android.graphics.Rect;
-import androidx.annotation.Nullable;
-import android.view.MotionEvent;
-import android.view.TouchDelegate;
-import android.view.View;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-public class TouchDelegateGroup extends TouchDelegate {
- private static final Rect USELESS_HACKY_RECT = new Rect();
- private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
- @Nullable private TouchDelegate mCurrentTouchDelegate;
- private boolean mEnabled;
-
- public TouchDelegateGroup(final View uselessHackyView) {
- super(USELESS_HACKY_RECT, uselessHackyView);
- }
-
- public void addTouchDelegate(final TouchDelegate touchDelegate) {
- mTouchDelegates.add(touchDelegate);
- }
-
- public void clearTouchDelegates() {
- mTouchDelegates.clear();
- mCurrentTouchDelegate = null;
- }
-
- @Override
- public boolean onTouchEvent(final MotionEvent event) {
- if (!mEnabled)
- return false;
-
- TouchDelegate delegate = null;
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- for (final TouchDelegate touchDelegate : mTouchDelegates) {
- if (touchDelegate.onTouchEvent(event)) {
- mCurrentTouchDelegate = touchDelegate;
- return true;
- }
- }
- break;
-
- case MotionEvent.ACTION_MOVE:
- delegate = mCurrentTouchDelegate;
- break;
-
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- delegate = mCurrentTouchDelegate;
- mCurrentTouchDelegate = null;
- break;
- }
-
- return delegate != null && delegate.onTouchEvent(event);
- }
-
- public void removeTouchDelegate(final TouchDelegate touchDelegate) {
- mTouchDelegates.remove(touchDelegate);
- if (mCurrentTouchDelegate == touchDelegate) {
- mCurrentTouchDelegate = null;
- }
- }
-
- public void setEnabled(final boolean enabled) {
- mEnabled = enabled;
- }
-}
diff --git a/app/src/main/java/com/wireguard/config/InetAddresses.java b/app/src/main/java/com/wireguard/config/InetAddresses.java
deleted file mode 100644
index 6396492e..00000000
--- a/app/src/main/java/com/wireguard/config/InetAddresses.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.config;
-
-import android.os.Build;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-
-/**
- * Utility methods for creating instances of {@link InetAddress}.
- */
-public final class InetAddresses {
- private static Method PARSER_METHOD;
-
-
- private static Method getParserMethod() {
- if (PARSER_METHOD != null)
- return PARSER_METHOD;
- try {
- // This method is only present on Android.
- // noinspection JavaReflectionMemberAccess
- PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
- } catch (final NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
- return PARSER_METHOD;
- }
-
- private InetAddresses() {
- // Prevent instantiation.
- }
-
- /**
- * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
- *
- * @param address a string representing the IP address
- * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
- */
- public static InetAddress parse(final String address) throws ParseException {
- if (address.isEmpty())
- throw new ParseException(InetAddress.class, address, "Empty address");
- try {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
- return (InetAddress) getParserMethod().invoke(null, address);
- else
- return android.net.InetAddresses.parseNumericAddress(address);
- } catch (final IllegalAccessException | InvocationTargetException e) {
- final Throwable cause = e.getCause();
- // Re-throw parsing exceptions with the original type, as callers might try to catch
- // them. On the other hand, callers cannot be expected to handle reflection failures.
- if (cause instanceof IllegalArgumentException)
- throw new ParseException(InetAddress.class, address, cause);
- throw new RuntimeException(e);
- } catch (final IllegalArgumentException e) {
- throw new ParseException(InetAddress.class, address, e);
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/util/Keyed.java b/app/src/main/java/com/wireguard/util/Keyed.java
deleted file mode 100644
index f31a43a2..00000000
--- a/app/src/main/java/com/wireguard/util/Keyed.java
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.util;
-
-/**
- * Interface for objects that have a identifying key of the given type.
- */
-
-public interface Keyed<K> {
- K getKey();
-}
diff --git a/app/src/main/java/com/wireguard/util/KeyedList.java b/app/src/main/java/com/wireguard/util/KeyedList.java
deleted file mode 100644
index c116c1da..00000000
--- a/app/src/main/java/com/wireguard/util/KeyedList.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.util;
-
-import androidx.annotation.Nullable;
-
-import java.util.Collection;
-import java.util.List;
-
-/**
- * A list containing elements that can be looked up by key. A {@code KeyedList} cannot contain
- * {@code null} elements.
- */
-
-public interface KeyedList<K, E extends Keyed<? extends K>> extends List<E> {
- boolean containsAllKeys(Collection<K> keys);
-
- boolean containsKey(K key);
-
- @Nullable
- E get(K key);
-
- @Nullable
- E getLast(K key);
-
- int indexOfKey(K key);
-
- int lastIndexOfKey(K key);
-}
diff --git a/app/src/main/java/com/wireguard/util/SortedKeyedList.java b/app/src/main/java/com/wireguard/util/SortedKeyedList.java
deleted file mode 100644
index b144fc85..00000000
--- a/app/src/main/java/com/wireguard/util/SortedKeyedList.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.util;
-
-import androidx.annotation.Nullable;
-
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Set;
-
-/**
- * A keyed list where all elements are sorted by the comparator returned by {@code comparator()}
- * applied to their keys.
- */
-
-public interface SortedKeyedList<K, E extends Keyed<? extends K>> extends KeyedList<K, E> {
- Comparator<? super K> comparator();
-
- @Nullable
- K firstKey();
-
- Set<K> keySet();
-
- @Nullable
- K lastKey();
-
- Collection<E> values();
-}
diff --git a/app/src/main/res/drawable/fab_label_background.xml b/app/src/main/res/drawable/fab_label_background.xml
deleted file mode 100644
index 92c42569..00000000
--- a/app/src/main/res/drawable/fab_label_background.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <corners android:radius="4dp" />
- <padding
- android:bottom="4dp"
- android:left="8dp"
- android:right="8dp"
- android:top="4dp" />
- <solid android:color="@color/fab_label_background_color" />
-</shape>
diff --git a/app/src/main/res/drawable/ic_action_edit_white.xml b/app/src/main/res/drawable/ic_action_edit_white.xml
deleted file mode 100644
index c1ce5043..00000000
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
- <path
- android:fillColor="#ffffff"
- 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/app/src/main/res/drawable/ic_action_open_white.xml b/app/src/main/res/drawable/ic_action_open_white.xml
deleted file mode 100644
index 74648d14..00000000
--- a/app/src/main/res/drawable/ic_action_open_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:viewportHeight="24"
- android:viewportWidth="24">
- <path
- android:fillColor="#ffffff"
- 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/app/src/main/res/layout-sw600dp/main_activity.xml b/app/src/main/res/layout-sw600dp/main_activity.xml
deleted file mode 100644
index 36f9eaff..00000000
--- a/app/src/main/res/layout-sw600dp/main_activity.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/master_detail_wrapper"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:baselineAligned="false"
- android:divider="?attr/dividerHorizontal"
- android:orientation="horizontal"
- android:showDividers="middle"
- tools:context=".activity.MainActivity">
-
- <fragment
- android:name="com.wireguard.android.fragment.TunnelListFragment"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="2"
- android:tag="LIST" />
-
- <FrameLayout
- android:id="@+id/detail_container"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="3" />
-</LinearLayout>
diff --git a/app/src/main/res/layout/app_list_dialog_fragment.xml b/app/src/main/res/layout/app_list_dialog_fragment.xml
deleted file mode 100644
index 50e795d5..00000000
--- a/app/src/main/res/layout/app_list_dialog_fragment.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <data>
-
- <import type="android.view.View" />
-
- <import type="com.wireguard.android.model.ApplicationData" />
-
- <variable
- name="fragment"
- type="com.wireguard.android.fragment.AppListDialogFragment" />
-
- <variable
- name="appData"
- type="com.wireguard.android.util.ObservableKeyedList&lt;String, ApplicationData&gt;" />
- </data>
-
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:minHeight="200dp">
-
- <ProgressBar
- android:id="@+id/progress_bar"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:indeterminate="true"
- android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}" />
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/app_list"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:items="@{appData}"
- app:layout="@{@layout/app_list_item}" />
-
- </FrameLayout>
-
-
-</layout>
diff --git a/app/src/main/res/layout/tunnel_detail_fragment.xml b/app/src/main/res/layout/tunnel_detail_fragment.xml
deleted file mode 100644
index 34332c2f..00000000
--- a/app/src/main/res/layout/tunnel_detail_fragment.xml
+++ /dev/null
@@ -1,138 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools">
-
- <data>
-
- <import type="com.wireguard.android.model.Tunnel.State" />
-
- <import type="com.wireguard.android.util.ClipboardUtils" />
-
- <variable
- name="fragment"
- type="com.wireguard.android.fragment.TunnelDetailFragment" />
-
- <variable
- name="tunnel"
- type="com.wireguard.android.model.Tunnel" />
-
- <variable
- name="config"
- type="com.wireguard.config.Config" />
- </data>
-
- <ScrollView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/colorBackground">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <androidx.cardview.widget.CardView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="4dp"
- android:layout_marginEnd="8dp"
- android:layout_marginStart="8dp"
- android:layout_marginTop="8dp"
- android:background="?android:attr/colorBackground"
- app:cardCornerRadius="4dp"
- app:cardElevation="2dp"
- app:contentPadding="8dp">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/interface_title"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_alignParentTop="true"
- android:text="@string/interface_title" />
-
- <com.wireguard.android.widget.ToggleSwitch
- android:id="@+id/tunnel_switch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/interface_title"
- android:layout_alignParentEnd="true"
- app:checked="@{tunnel.state == State.UP}"
- app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
-
- <TextView
- android:id="@+id/interface_name_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/interface_title"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/interface_name_text"
- android:text="@string/name" />
-
- <TextView
- android:id="@+id/interface_name_text"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/interface_name_label"
- android:text="@{tunnel.name}" />
-
- <TextView
- android:id="@+id/public_key_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@id/interface_name_text"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/public_key_text"
- android:text="@string/public_key" />
-
- <TextView
- android:id="@+id/public_key_text"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/public_key_label"
- android:contentDescription="@string/public_key_description"
- android:ellipsize="end"
- android:maxLines="1"
- android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.keyPair.publicKey.toBase64}" />
-
- <TextView
- android:id="@+id/addresses_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@id/public_key_text"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/addresses_text"
- android:text="@string/addresses" />
-
- <TextView
- android:id="@+id/addresses_text"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/addresses_label"
- android:contentDescription="@string/addresses"
- android:text="@{config.interface.addresses}" />
- </RelativeLayout>
- </androidx.cardview.widget.CardView>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="4dp"
- android:divider="@null"
- android:orientation="vertical"
- app:items="@{config.peers}"
- app:layout="@{@layout/tunnel_detail_peer}"
- tools:ignore="UselessLeaf" />
- </LinearLayout>
- </ScrollView>
-</layout>
diff --git a/app/src/main/res/layout/tunnel_detail_peer.xml b/app/src/main/res/layout/tunnel_detail_peer.xml
deleted file mode 100644
index f528a8df..00000000
--- a/app/src/main/res/layout/tunnel_detail_peer.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <data>
-
- <import type="com.wireguard.android.util.ClipboardUtils" />
-
- <variable
- name="item"
- type="com.wireguard.config.Peer" />
- </data>
-
- <androidx.cardview.widget.CardView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="4dp"
- android:layout_marginEnd="8dp"
- android:layout_marginStart="8dp"
- android:layout_marginTop="4dp"
- android:background="?android:attr/colorBackground"
- app:cardCornerRadius="4dp"
- app:cardElevation="2dp"
- app:contentPadding="8dp">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/peer_title"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:text="@string/peer" />
-
- <TextView
- android:id="@+id/public_key_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@id/peer_title"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/public_key_text"
- android:text="@string/public_key" />
-
- <TextView
- android:id="@+id/public_key_text"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/public_key_label"
- android:contentDescription="@string/public_key_description"
- android:ellipsize="end"
- android:maxLines="1"
- android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{item.publicKey.toBase64}" />
-
- <TextView
- android:id="@+id/allowed_ips_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/public_key_text"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/allowed_ips_text"
- android:text="@string/allowed_ips" />
-
- <TextView
- android:id="@+id/allowed_ips_text"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/allowed_ips_label"
- android:text="@{item.allowedIps}" />
-
- <TextView
- android:id="@+id/endpoint_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/allowed_ips_text"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/endpoint_text"
- android:text="@string/endpoint" />
-
- <TextView
- android:id="@+id/endpoint_text"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/endpoint_label"
- android:text="@{item.endpoint}" />
- </RelativeLayout>
- </androidx.cardview.widget.CardView>
-</layout>
diff --git a/app/src/main/res/layout/tunnel_editor_fragment.xml b/app/src/main/res/layout/tunnel_editor_fragment.xml
deleted file mode 100644
index db91df2b..00000000
--- a/app/src/main/res/layout/tunnel_editor_fragment.xml
+++ /dev/null
@@ -1,254 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools">
-
- <data>
-
- <import type="com.wireguard.android.util.ClipboardUtils" />
-
- <import type="com.wireguard.android.widget.KeyInputFilter" />
-
- <import type="com.wireguard.android.widget.NameInputFilter" />
-
- <variable
- name="fragment"
- type="com.wireguard.android.fragment.TunnelEditorFragment" />
-
- <variable
- name="config"
- type="com.wireguard.android.viewmodel.ConfigProxy" />
-
- <variable
- name="name"
- type="String" />
- </data>
-
- <androidx.coordinatorlayout.widget.CoordinatorLayout
- android:id="@+id/main_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/colorBackground">
-
- <ScrollView
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <androidx.cardview.widget.CardView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="4dp"
- android:layout_marginEnd="8dp"
- android:layout_marginStart="8dp"
- android:layout_marginTop="8dp"
- android:background="?android:attr/colorBackground"
- app:cardCornerRadius="4dp"
- app:cardElevation="2dp"
- app:contentPadding="8dp">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/interface_title"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:text="@string/interface_title" />
-
- <TextView
- android:id="@+id/interface_name_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/interface_title"
- android:layout_marginTop="8dp"
- android:labelFor="@+id/interface_name_text"
- android:text="@string/name" />
-
- <EditText
- android:id="@+id/interface_name_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/interface_name_label"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={name}"
- app:filter="@{NameInputFilter.newInstance()}" />
-
- <TextView
- android:id="@+id/private_key_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/interface_name_text"
- android:labelFor="@+id/private_key_text"
- android:text="@string/private_key" />
-
- <EditText
- android:id="@+id/private_key_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/private_key_label"
- android:layout_toStartOf="@+id/generate_private_key_button"
- android:contentDescription="@string/public_key_description"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={config.interface.privateKey}"
- app:filter="@{KeyInputFilter.newInstance()}" />
-
- <Button
- android:id="@+id/generate_private_key_button"
- style="@style/Widget.AppCompat.Button.Borderless.Colored"
- android:layout_width="96dp"
- android:layout_height="wrap_content"
- android:layout_alignBottom="@id/private_key_text"
- android:layout_alignParentEnd="true"
- android:layout_below="@+id/private_key_label"
- android:onClick="@{() -> config.interface.generateKeyPair()}"
- android:text="@string/generate" />
-
- <TextView
- android:id="@+id/public_key_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/private_key_text"
- android:labelFor="@+id/public_key_text"
- android:text="@string/public_key" />
-
- <TextView
- android:id="@+id/public_key_text"
- style="?attr/editTextStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/public_key_label"
- android:contentDescription="@string/public_key_description"
- android:ellipsize="end"
- android:focusable="false"
- android:hint="@string/hint_generated"
- android:maxLines="1"
- android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.publicKey}" />
-
- <TextView
- android:id="@+id/addresses_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/public_key_text"
- android:layout_toStartOf="@+id/listen_port_label"
- android:labelFor="@+id/addresses_text"
- android:text="@string/addresses" />
-
- <EditText
- android:id="@+id/addresses_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/addresses_label"
- android:layout_toStartOf="@+id/listen_port_text"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={config.interface.addresses}" />
-
- <TextView
- android:id="@+id/listen_port_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/addresses_label"
- android:layout_alignParentEnd="true"
- android:layout_alignStart="@+id/generate_private_key_button"
- android:labelFor="@+id/listen_port_text"
- android:text="@string/listen_port" />
-
- <EditText
- android:id="@+id/listen_port_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/addresses_text"
- android:layout_alignParentEnd="true"
- android:layout_alignStart="@+id/generate_private_key_button"
- android:hint="@string/hint_random"
- android:inputType="number"
- android:text="@={config.interface.listenPort}"
- android:textAlignment="center" />
-
- <TextView
- android:id="@+id/dns_servers_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/addresses_text"
- android:layout_toStartOf="@+id/mtu_label"
- android:labelFor="@+id/dns_servers_text"
- android:text="@string/dns_servers" />
-
- <EditText
- android:id="@+id/dns_servers_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/dns_servers_label"
- android:layout_toStartOf="@+id/mtu_text"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={config.interface.dnsServers}" />
-
- <TextView
- android:id="@+id/mtu_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/dns_servers_label"
- android:layout_alignParentEnd="true"
- android:layout_alignStart="@+id/generate_private_key_button"
- android:labelFor="@+id/mtu_text"
- android:text="@string/mtu" />
-
- <EditText
- android:id="@+id/mtu_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/dns_servers_text"
- android:layout_alignParentEnd="true"
- android:layout_alignStart="@+id/generate_private_key_button"
- android:hint="@string/hint_automatic"
- android:inputType="number"
- android:text="@={config.interface.mtu}"
- android:textAlignment="center" />
-
- <Button
- android:id="@+id/set_excluded_applications"
- style="@style/Widget.AppCompat.Button.Borderless.Colored"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@+id/dns_servers_text"
- android:layout_marginLeft="-8dp"
- android:onClick="@{fragment::onRequestSetExcludedApplications}"
- android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" />
- </RelativeLayout>
- </androidx.cardview.widget.CardView>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:divider="@null"
- android:orientation="vertical"
- app:items="@{config.peers}"
- app:layout="@{@layout/tunnel_editor_peer}"
- tools:ignore="UselessLeaf" />
-
- <Button
- style="@style/Widget.AppCompat.Button.Colored"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginStart="4dp"
- android:onClick="@{() -> config.addPeer()}"
- android:text="@string/add_peer" />
- </LinearLayout>
- </ScrollView>
- </androidx.coordinatorlayout.widget.CoordinatorLayout>
-</layout>
diff --git a/app/src/main/res/layout/tunnel_editor_peer.xml b/app/src/main/res/layout/tunnel_editor_peer.xml
deleted file mode 100644
index 175c174e..00000000
--- a/app/src/main/res/layout/tunnel_editor_peer.xml
+++ /dev/null
@@ -1,161 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <data>
-
- <import type="android.view.View" />
-
- <import type="com.wireguard.android.widget.KeyInputFilter" />
-
- <variable
- name="collection"
- type="androidx.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
-
- <variable
- name="item"
- type="com.wireguard.android.viewmodel.PeerProxy" />
- </data>
-
- <androidx.cardview.widget.CardView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="4dp"
- android:layout_marginEnd="8dp"
- android:layout_marginStart="8dp"
- android:layout_marginTop="4dp"
- android:background="?android:attr/colorBackground"
- app:cardCornerRadius="4dp"
- app:cardElevation="2dp"
- app:contentPadding="8dp">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:id="@+id/peer_title"
- style="?android:attr/textAppearanceMedium"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_alignParentTop="true"
- android:layout_marginBottom="8dp"
- android:layout_toStartOf="@+id/peer_action_delete"
- android:text="@string/peer" />
-
- <ImageButton
- android:id="@+id/peer_action_delete"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_alignParentTop="true"
- android:background="@null"
- android:contentDescription="@string/delete"
- android:onClick="@{() -> item.unbind()}"
- android:src="@drawable/ic_action_delete" />
-
- <TextView
- android:id="@+id/public_key_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@id/peer_title"
- android:labelFor="@+id/public_key_text"
- android:text="@string/public_key" />
-
- <EditText
- android:id="@+id/public_key_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/public_key_label"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={item.publicKey}"
- app:filter="@{KeyInputFilter.newInstance()}" />
-
- <TextView
- android:id="@+id/pre_shared_key_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/public_key_text"
- android:labelFor="@+id/pre_shared_key_text"
- android:text="@string/pre_shared_key" />
-
- <EditText
- android:id="@+id/pre_shared_key_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/pre_shared_key_label"
- android:hint="@string/hint_optional"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={item.preSharedKey}" />
-
- <TextView
- android:id="@+id/allowed_ips_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/pre_shared_key_text"
- android:layout_toStartOf="@+id/exclude_private_ips"
- android:labelFor="@+id/allowed_ips_text"
- android:text="@string/allowed_ips" />
-
- <CheckBox
- android:id="@+id/exclude_private_ips"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/allowed_ips_label"
- android:layout_alignParentEnd="true"
- android:checked="@={item.excludingPrivateIps}"
- android:text="@string/exclude_private_ips"
- android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}" />
-
- <EditText
- android:id="@+id/allowed_ips_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_below="@+id/allowed_ips_label"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={item.allowedIps}" />
-
- <TextView
- android:id="@+id/endpoint_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/allowed_ips_text"
- android:layout_toStartOf="@+id/persistent_keepalive_label"
- android:labelFor="@+id/endpoint_text"
- android:text="@string/endpoint" />
-
- <EditText
- android:id="@+id/endpoint_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentStart="true"
- android:layout_below="@+id/endpoint_label"
- android:layout_toStartOf="@+id/persistent_keepalive_text"
- android:inputType="textNoSuggestions|textVisiblePassword"
- android:text="@={item.endpoint}" />
-
- <TextView
- android:id="@+id/persistent_keepalive_label"
- android:layout_width="96dp"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/endpoint_label"
- android:layout_alignParentEnd="true"
- android:labelFor="@+id/persistent_keepalive_text"
- android:text="@string/persistent_keepalive" />
-
- <EditText
- android:id="@+id/persistent_keepalive_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignBaseline="@+id/endpoint_text"
- android:layout_alignParentEnd="true"
- android:layout_alignStart="@+id/persistent_keepalive_label"
- android:hint="@string/hint_optional"
- android:inputType="number"
- android:text="@={item.persistentKeepalive}"
- android:textAlignment="center" />
- </RelativeLayout>
- </androidx.cardview.widget.CardView>
-</layout>
diff --git a/app/src/main/res/values-ldrtl/fab.xml b/app/src/main/res/values-ldrtl/fab.xml
deleted file mode 100644
index f612440c..00000000
--- a/app/src/main/res/values-ldrtl/fab.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <integer name="label_position">1</integer>
-</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
deleted file mode 100644
index 247d8e33..00000000
--- a/app/src/main/res/values-night/colors.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
- <color name="fab_label_text_color">#000000</color>
- <color name="fab_label_background_color">#bbbbbb</color>
-
- <color name="accent">#5e97f6</color>
-</resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index bc0fbdb0..00000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
- <color name="fab_label_text_color">#ffffff</color>
- <color name="fab_label_background_color">#444444</color>
-
- <color name="accent">#2196F3</color>
-</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index 92be463c..00000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <dimen name="fab_margin">16dp</dimen>
-</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/fab.xml b/app/src/main/res/values/fab.xml
deleted file mode 100644
index 8fbdc724..00000000
--- a/app/src/main/res/values/fab.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <item name="fab_expand_menu_button" type="id" />
- <item name="fab_label" type="id" />
-
- <dimen name="fab_shadow_offset">3dp</dimen>
- <dimen name="fab_shadow_radius">9dp</dimen>
-
- <dimen name="fab_stroke_width">1dp</dimen>
-
- <dimen name="fab_actions_spacing">24dp</dimen>
- <dimen name="fab_labels_margin">8dp</dimen>
-
- <declare-styleable name="LabeledFloatingActionButton">
- <attr name="fab_title" format="string" />
- </declare-styleable>
- <declare-styleable name="FloatingActionsMenu">
- <attr name="fab_labelStyle" format="reference" />
- <attr name="fab_labelsPosition" format="enum">
- <enum name="left" value="0" />
- <enum name="right" value="1" />
- </attr>
- <attr name="fab_expandDirection" format="enum">
- <enum name="up" value="0" />
- <enum name="down" value="1" />
- <enum name="left" value="2" />
- <enum name="right" value="3" />
- </attr>
- </declare-styleable>
- <integer name="label_position">0</integer>
-</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index 2605691d..00000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
- <item name="colorAccent">@color/accent</item>
- </style>
-
- <style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
- <item name="android:background">@drawable/fab_label_background</item>
- <item name="android:textColor">@color/fab_label_text_color</item>
- </style>
-
-</resources>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
deleted file mode 100644
index 9c09ae89..00000000
--- a/app/src/main/res/xml/preferences.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
- <com.wireguard.android.preference.VersionPreference android:icon="@mipmap/ic_launcher" />
- <CheckBoxPreference
- android:defaultValue="false"
- android:key="restore_on_boot"
- android:summary="@string/restore_on_boot_summary"
- android:title="@string/restore_on_boot_title" />
- <com.wireguard.android.preference.ModuleDownloaderPreference android:key="module_downloader" />
- <com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
- <com.wireguard.android.preference.ZipExporterPreference />
- <com.wireguard.android.preference.LogExporterPreference />
- <CheckBoxPreference
- android:defaultValue="false"
- android:key="dark_theme"
- android:summaryOff="@string/dark_theme_summary_off"
- android:summaryOn="@string/dark_theme_summary_on"
- android:title="@string/dark_theme_title" />
-</androidx.preference.PreferenceScreen>
diff --git a/app/tools/CMakeLists.txt b/app/tools/CMakeLists.txt
deleted file mode 100644
index 2a04901f..00000000
--- a/app/tools/CMakeLists.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-#
-# Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
-
-cmake_minimum_required(VERSION 3.4.1)
-set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
-
-# Work around https://github.com/android-ndk/ndk/issues/602
-set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold")
-
-add_executable(libwg-quick.so wireguard/src/tools/wg-quick/android.c ndk-compat/compat.c)
-target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
-target_link_libraries(libwg-quick.so -ldl)
-
-file(GLOB WG_SOURCES wireguard/src/tools/*.c libmnl/src/*.c ndk-compat/compat.c)
-add_executable(libwg.so ${WG_SOURCES})
-target_compile_options(libwg.so PUBLIC -idirafter "${CMAKE_CURRENT_SOURCE_DIR}/libmnl/include/" -I "${CMAKE_CURRENT_SOURCE_DIR}/wireguard/src/tools/" -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\" -Wno-pointer-arith -Wno-unused-parameter)
-
-add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
- ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
- ANDROID_C_COMPILER=${ANDROID_C_COMPILER}
- ANDROID_TOOLCHAIN_ROOT=${ANDROID_TOOLCHAIN_ROOT}
- ANDROID_LLVM_TRIPLE=${ANDROID_LLVM_TRIPLE}
- ANDROID_SYSROOT=${ANDROID_SYSROOT}
- ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
- CFLAGS=${CMAKE_C_FLAGS}\ -Wno-unused-command-line-argument
- LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}\ -fuse-ld=gold
- DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
-)
-# Hack to make it actually build as part of the default target
-add_dependencies(libwg.so libwg-go.so)
diff --git a/app/tools/libmnl b/app/tools/libmnl
deleted file mode 160000
-Subproject 0930a63252958f40bb0f9d09de86985c25cea03
diff --git a/app/tools/libwg-go/Makefile b/app/tools/libwg-go/Makefile
deleted file mode 100644
index cb241191..00000000
--- a/app/tools/libwg-go/Makefile
+++ /dev/null
@@ -1,35 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-#
-# Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
-
-BUILDDIR ?= $(CURDIR)/build
-DESTDIR ?= $(CURDIR)/out
-
-NDK_GO_ARCH_MAP_x86 := 386
-NDK_GO_ARCH_MAP_x86_64 := amd64
-NDK_GO_ARCH_MAP_arm := arm
-NDK_GO_ARCH_MAP_arm64 := arm64
-NDK_GO_ARCH_MAP_mips := mipsx
-NDK_GO_ARCH_MAP_mips64 := mips64x
-
-CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
-export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
-export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
-export CC := $(ANDROID_C_COMPILER)
-export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
-export GOOS := android
-export CGO_ENABLED := 1
-
-DESIRED_GO_VERSION := 1.13.1
-
-default: $(DESTDIR)/libwg-go.so
-
-$(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared:
- mkdir -p "$(dir $@)"
- curl "https://dl.google.com/go/go$(DESIRED_GO_VERSION).$(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)).tar.gz" | tar -C "$(dir $@)" --strip-components=1 -xzf -
- patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff
- touch "$@"
-
-$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/bin/:$(PATH)
-$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared go.mod
- go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -trimpath -o "$@" -buildmode c-shared
diff --git a/app/tools/libwg-go/api-android.go b/app/tools/libwg-go/api-android.go
deleted file mode 100644
index 7e951b9c..00000000
--- a/app/tools/libwg-go/api-android.go
+++ /dev/null
@@ -1,176 +0,0 @@
-/* SPDX-License-Identifier: Apache-2.0
- *
- * Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
- */
-
-package main
-
-// #cgo LDFLAGS: -llog
-// #include <android/log.h>
-import "C"
-
-import (
- "bufio"
- "golang.org/x/sys/unix"
- "golang.zx2c4.com/wireguard/device"
- "golang.zx2c4.com/wireguard/ipc"
- "golang.zx2c4.com/wireguard/tun"
- "log"
- "math"
- "net"
- "os"
- "os/signal"
- "runtime"
- "strings"
- "unsafe"
-)
-
-type AndroidLogger struct {
- level C.int
- interfaceName string
-}
-
-func (l AndroidLogger) Write(p []byte) (int, error) {
- C.__android_log_write(l.level, C.CString("WireGuard/GoBackend/"+l.interfaceName), C.CString(string(p)))
- return len(p), nil
-}
-
-type TunnelHandle struct {
- device *device.Device
- uapi net.Listener
-}
-
-var tunnelHandles map[int32]TunnelHandle
-
-func init() {
- device.RoamingDisabled = true
- tunnelHandles = make(map[int32]TunnelHandle)
- signals := make(chan os.Signal)
- signal.Notify(signals, unix.SIGUSR2)
- go func() {
- buf := make([]byte, os.Getpagesize())
- for {
- select {
- case <-signals:
- n := runtime.Stack(buf, true)
- buf[n] = 0
- C.__android_log_write(C.ANDROID_LOG_ERROR, C.CString("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
- }
- }
- }()
-}
-
-//export wgTurnOn
-func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
- interfaceName := string([]byte(ifnameRef))
-
- logger := &device.Logger{
- Debug: log.New(&AndroidLogger{level: C.ANDROID_LOG_DEBUG, interfaceName: interfaceName}, "", 0),
- Info: log.New(&AndroidLogger{level: C.ANDROID_LOG_INFO, interfaceName: interfaceName}, "", 0),
- Error: log.New(&AndroidLogger{level: C.ANDROID_LOG_ERROR, interfaceName: interfaceName}, "", 0),
- }
-
- logger.Debug.Println("Debug log enabled")
-
- tun, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
- if err != nil {
- unix.Close(int(tunFd))
- logger.Error.Println(err)
- return -1
- }
-
- logger.Info.Println("Attaching to interface", name)
- device := device.NewDevice(tun, logger)
-
- setError := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings)))
- if setError != nil {
- unix.Close(int(tunFd))
- logger.Error.Println(setError)
- return -1
- }
-
- var uapi net.Listener
-
- uapiFile, err := ipc.UAPIOpen(name)
- if err != nil {
- logger.Error.Println(err)
- } else {
- uapi, err = ipc.UAPIListen(name, uapiFile)
- if err != nil {
- uapiFile.Close()
- logger.Error.Println(err)
- } else {
- go func() {
- for {
- conn, err := uapi.Accept()
- if err != nil {
- return
- }
- go device.IpcHandle(conn)
- }
- }()
- }
- }
-
- device.Up()
- logger.Info.Println("Device started")
-
- var i int32
- for i = 0; i < math.MaxInt32; i++ {
- if _, exists := tunnelHandles[i]; !exists {
- break
- }
- }
- if i == math.MaxInt32 {
- unix.Close(int(tunFd))
- return -1
- }
- tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
- return i
-}
-
-//export wgTurnOff
-func wgTurnOff(tunnelHandle int32) {
- handle, ok := tunnelHandles[tunnelHandle]
- if !ok {
- return
- }
- delete(tunnelHandles, tunnelHandle)
- if handle.uapi != nil {
- handle.uapi.Close()
- }
- handle.device.Close()
-}
-
-//export wgGetSocketV4
-func wgGetSocketV4(tunnelHandle int32) int32 {
- handle, ok := tunnelHandles[tunnelHandle]
- if !ok {
- return -1
- }
- fd, err := handle.device.PeekLookAtSocketFd4()
- if err != nil {
- return -1
- }
- return int32(fd)
-}
-
-//export wgGetSocketV6
-func wgGetSocketV6(tunnelHandle int32) int32 {
- handle, ok := tunnelHandles[tunnelHandle]
- if !ok {
- return -1
- }
- fd, err := handle.device.PeekLookAtSocketFd6()
- if err != nil {
- return -1
- }
- return int32(fd)
-}
-
-//export wgVersion
-func wgVersion() *C.char {
- return C.CString(device.WireGuardGoVersion)
-}
-
-func main() {}
diff --git a/app/tools/libwg-go/go.mod b/app/tools/libwg-go/go.mod
deleted file mode 100644
index f52e4250..00000000
--- a/app/tools/libwg-go/go.mod
+++ /dev/null
@@ -1,10 +0,0 @@
-module golang.zx2c4.com/wireguard/android
-
-go 1.12
-
-require (
- golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
- golang.org/x/net v0.0.0-20191011234655-491137f69257 // indirect
- golang.org/x/sys v0.0.0-20191010194322-b09406accb47
- golang.zx2c4.com/wireguard v0.0.20191012
-)
diff --git a/app/tools/libwg-go/go.sum b/app/tools/libwg-go/go.sum
deleted file mode 100644
index 719f3ec1..00000000
--- a/app/tools/libwg-go/go.sum
+++ /dev/null
@@ -1,19 +0,0 @@
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo24shrbK0e11QEOkTIg=
-golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.zx2c4.com/wireguard v0.0.20191012 h1:sdX+y3hrHkW8KJkjY7ZgzpT5Tqo8XnBkH55U1klphko=
-golang.zx2c4.com/wireguard v0.0.20191012/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
diff --git a/app/tools/ndk-compat/compat.c b/app/tools/ndk-compat/compat.c
deleted file mode 100644
index 7cc99fc4..00000000
--- a/app/tools/ndk-compat/compat.c
+++ /dev/null
@@ -1,77 +0,0 @@
-/* SPDX-License-Identifier: BSD
- *
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- *
- */
-
-#define FILE_IS_EMPTY
-
-#if defined(__ANDROID_API__) && __ANDROID_API__ < 18
-#undef FILE_IS_EMPTY
-#include <stdio.h>
-#include <stdlib.h>
-
-ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp)
-{
- char *ptr, *eptr;
-
- if (*buf == NULL || *bufsiz == 0) {
- *bufsiz = BUFSIZ;
- if ((*buf = malloc(*bufsiz)) == NULL)
- return -1;
- }
-
- for (ptr = *buf, eptr = *buf + *bufsiz;;) {
- int c = fgetc(fp);
- if (c == -1) {
- if (feof(fp)) {
- ssize_t diff = (ssize_t)(ptr - *buf);
- if (diff != 0) {
- *ptr = '\0';
- return diff;
- }
- }
- return -1;
- }
- *ptr++ = c;
- if (c == delimiter) {
- *ptr = '\0';
- return ptr - *buf;
- }
- if (ptr + 2 >= eptr) {
- char *nbuf;
- size_t nbufsiz = *bufsiz * 2;
- ssize_t d = ptr - *buf;
- if ((nbuf = realloc(*buf, nbufsiz)) == NULL)
- return -1;
- *buf = nbuf;
- *bufsiz = nbufsiz;
- eptr = nbuf + nbufsiz;
- ptr = nbuf + d;
- }
- }
-}
-
-ssize_t getline(char **buf, size_t *bufsiz, FILE *fp)
-{
- return getdelim(buf, bufsiz, '\n', fp);
-}
-#endif
-
-#if defined(__ANDROID_API__) && __ANDROID_API__ < 24
-#undef FILE_IS_EMPTY
-#include <string.h>
-
-char *strchrnul(const char *s, int c)
-{
- char *x = strchr(s, c);
- if (!x)
- return (char *)s + strlen(s);
- return x;
-}
-#endif
-
-#ifdef FILE_IS_EMPTY
-#undef FILE_IS_EMPTY
-static char ____x __attribute__((unused));
-#endif
diff --git a/app/tools/ndk-compat/compat.h b/app/tools/ndk-compat/compat.h
deleted file mode 100644
index 52f6c127..00000000
--- a/app/tools/ndk-compat/compat.h
+++ /dev/null
@@ -1,16 +0,0 @@
-/* SPDX-License-Identifier: BSD
- *
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- *
- */
-
-#if defined(__ANDROID_API__) && __ANDROID_API__ < 18
-#include <stdio.h>
-ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp);
-ssize_t getline(char **buf, size_t *bufsiz, FILE *fp);
-#endif
-
-#if defined(__ANDROID_API__) && __ANDROID_API__ < 24
-char *strchrnul(const char *s, int c);
-#endif
-
diff --git a/app/tools/wireguard b/app/tools/wireguard
deleted file mode 160000
-Subproject 0b27b1d315e9d1e6edd0c5eadbd2010abd1746c
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index e22f92cf..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-allprojects {
- repositories {
- google()
- jcenter()
- }
-}
-
-buildscript {
- dependencies {
- classpath 'com.android.tools.build:gradle:3.5.0'
- }
- repositories {
- google()
- jcenter()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
-
-tasks {
- wrapper {
- gradleVersion = "5.6.2"
- distributionType = Wrapper.DistributionType.ALL
- }
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..fbcca451
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.kapt) apply false
+}
+
+tasks {
+ wrapper {
+ gradleVersion = "8.3"
+ distributionSha256Sum = "591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225"
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 9e6fce10..efc1fe18 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,19 +1,73 @@
-# Project-wide Gradle settings.
+wireguardVersionCode=510
+wireguardVersionName=1.0.20231018
+wireguardPackageName=com.wireguard.android
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+org.gradle.parallel=true
+org.gradle.configureondemand=true
+org.gradle.caching=true
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Enable Kotlin incremental compilation
+kotlin.incremental=true
+
+# Enable AndroidX support
+android.useAndroidX=true
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-android.enableJetifier=true
-android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
+# Turn off AP discovery in compile path to enable compile avoidance
+kapt.include.compile.classpath=false
+
+# Enable non-transitive R class namespacing where each library only contains
+# references to the resources it declares instead of declarations plus all
+# transitive dependency references.
+android.nonTransitiveRClass=true
+
+# Experimental AGP flags
+# Generate compile-time only R class for app modules.
+android.enableAppCompileTimeRClass=true
+# Keep AAPT2 daemons alive between incremental builds.
+android.keepWorkerActionServicesBetweenBuilds=true
+# Make R fields non-final to improve build speeds.
+# http://tools.android.com/tips/non-constant-fields
+android.nonFinalResIds=true
+# Enable the newly refactored resource shrinker.
+android.experimental.enableNewResourceShrinker=true
+# Enable precise shrinking in the new resource shrinker.
+android.experimental.enableNewResourceShrinker.preciseShrinking=true
+# Generate manifest class as a .class directly rather than a Java source file.
+android.generateManifestClass=true
+# Generate the text map of source sets and absolute paths to allow
+# generating relative paths from absolute paths later in the build.
+android.experimental.enableSourceSetPathsMap=true
+# Use relative paths for better Gradle caching of library build tasks
+android.experimental.cacheCompileLibResources=true
+
+# Default Android build features
+# Disable BuildConfig generation by default
+android.defaults.buildfeatures.buildconfig=false
+# Disable AIDL stub generation by default
+android.defaults.buildfeatures.aidl=false
+# Disable RenderScript compilation by default
+android.defaults.buildfeatures.renderscript=false
+# Disable resource values generation by default in libraries
+android.defaults.buildfeatures.resvalues=false
+# Disable shader compilation by default
+android.defaults.buildfeatures.shaders=false
+# Disable Android resource processing by default
+android.library.defaults.buildfeatures.androidresources=false
+
+# Suppress warnings for some features that aren't yet stabilized
+android.suppressUnsupportedOptionWarnings=android.keepWorkerActionServicesBetweenBuilds,\
+ android.experimental.enableNewResourceShrinker.preciseShrinking,\
+ android.enableAppCompileTimeRClass,\
+ android.suppressUnsupportedOptionWarnings
+
+# OSSRH sometimes struggles with slow deployments, so this makes Gradle
+# more tolerant to those delays.
+systemProp.org.gradle.internal.http.connectionTimeout=500000
+systemProp.org.gradle.internal.http.socketTimeout=500000
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 00000000..d8527046
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,29 @@
+[versions]
+agp = "8.2.0-beta06"
+kotlin = "1.9.0"
+
+[libraries]
+androidx-activity-ktx = "androidx.activity:activity-ktx:1.8.0"
+androidx-annotation = "androidx.annotation:annotation:1.7.0"
+androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
+androidx-biometric = "androidx.biometric:biometric:1.1.0"
+androidx-collection = "androidx.collection:collection:1.3.0"
+androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
+androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
+androidx-core-ktx = "androidx.core:core-ktx:1.12.0"
+androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.0.0"
+androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.6.1"
+androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
+androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.1"
+desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3"
+google-material = "com.google.android.material:material:1.10.0"
+jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
+junit = "junit:junit:4.13.2"
+kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
+zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 5c2d1cf0..7f93135c 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ca9d6281..864d6c47 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
+distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 83f2acfd..0adc8e1a 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,78 +17,111 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -97,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=$(save "$@")
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
fi
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
exec "$JAVACMD" "$@"
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index e7b4def4..00000000
--- a/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..91bc0b90
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,22 @@
+@file:Suppress("UnstableApiUsage")
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "wireguard-android"
+
+include(":tunnel")
+include(":ui")
diff --git a/sync-crowdin.sh b/sync-crowdin.sh
new file mode 100755
index 00000000..043e64f3
--- /dev/null
+++ b/sync-crowdin.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+set -ex
+curl -Lo - https://crowdin.com/backend/download/project/wireguard.zip | bsdtar -C ui/src/main/res -x -f - --strip-components 5 wireguard-android
+find ui/src/main/res -name strings.xml -exec bash -c '[[ $(xmllint --xpath "count(//resources/*)" {}) -ne 0 ]] || rm -rf "$(dirname {})"' \;
diff --git a/tunnel/build.gradle.kts b/tunnel/build.gradle.kts
new file mode 100644
index 00000000..589d72da
--- /dev/null
+++ b/tunnel/build.gradle.kts
@@ -0,0 +1,128 @@
+@file:Suppress("UnstableApiUsage")
+
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+
+val pkg: String = providers.gradleProperty("wireguardPackageName").get()
+
+plugins {
+ alias(libs.plugins.android.library)
+ `maven-publish`
+ signing
+}
+
+android {
+ compileSdk = 34
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ namespace = "${pkg}.tunnel"
+ defaultConfig {
+ minSdk = 21
+ }
+ externalNativeBuild {
+ cmake {
+ path("tools/CMakeLists.txt")
+ }
+ }
+ testOptions.unitTests.all {
+ it.testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
+ }
+ buildTypes {
+ all {
+ externalNativeBuild {
+ cmake {
+ targets("libwg-go.so", "libwg.so", "libwg-quick.so")
+ arguments("-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}")
+ }
+ }
+ }
+ release {
+ externalNativeBuild {
+ cmake {
+ arguments("-DANDROID_PACKAGE_NAME=${pkg}")
+ }
+ }
+ }
+ debug {
+ externalNativeBuild {
+ cmake {
+ arguments("-DANDROID_PACKAGE_NAME=${pkg}.debug")
+ }
+ }
+ }
+ }
+ lint {
+ disable += "LongLogTag"
+ disable += "NewApi"
+ }
+ publishing {
+ singleVariant("release") {
+ withJavadocJar()
+ withSourcesJar()
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.annotation)
+ implementation(libs.androidx.collection)
+ compileOnly(libs.jsr305)
+ testImplementation(libs.junit)
+}
+
+publishing {
+ publications {
+ register<MavenPublication>("release") {
+ groupId = pkg
+ artifactId = "tunnel"
+ version = providers.gradleProperty("wireguardVersionName").get()
+ afterEvaluate {
+ from(components["release"])
+ }
+ pom {
+ name.set("WireGuard Tunnel Library")
+ description.set("Embeddable tunnel library for WireGuard for Android")
+ url.set("https://www.wireguard.com/")
+
+ licenses {
+ license {
+ name.set("The Apache Software License, Version 2.0")
+ url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
+ distribution.set("repo")
+ }
+ }
+ scm {
+ connection.set("scm:git:https://git.zx2c4.com/wireguard-android")
+ developerConnection.set("scm:git:https://git.zx2c4.com/wireguard-android")
+ url.set("https://git.zx2c4.com/wireguard-android")
+ }
+ developers {
+ organization {
+ name.set("WireGuard")
+ url.set("https://www.wireguard.com/")
+ }
+ developer {
+ name.set("WireGuard")
+ email.set("team@wireguard.com")
+ }
+ }
+ }
+ }
+ }
+ repositories {
+ maven {
+ name = "sonatype"
+ url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
+ credentials {
+ username = providers.environmentVariable("SONATYPE_USER").orNull
+ password = providers.environmentVariable("SONATYPE_PASSWORD").orNull
+ }
+ }
+ }
+}
+
+signing {
+ useGpgCmd()
+ sign(publishing.publications)
+}
diff --git a/tunnel/src/main/AndroidManifest.xml b/tunnel/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..99509a20
--- /dev/null
+++ b/tunnel/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <service
+ android:name="com.wireguard.android.backend.GoBackend$VpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.net.VpnService" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/app/src/main/java/com/wireguard/android/backend/Backend.java b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
index a25bfd04..edf98b9e 100644
--- a/app/src/main/java/com/wireguard/android/backend/Backend.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
@@ -1,47 +1,38 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.backend;
-import com.wireguard.android.model.Tunnel;
-import com.wireguard.android.model.Tunnel.State;
-import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
+import com.wireguard.util.NonNullForAll;
import java.util.Set;
+import androidx.annotation.Nullable;
+
/**
* Interface for implementations of the WireGuard secure network tunnel.
*/
+@NonNullForAll
public interface Backend {
/**
- * Update the volatile configuration of a running tunnel and return the resulting configuration.
- * If the tunnel is not up, return the configuration that would result (if known), or else
- * simply return the given configuration.
- *
- * @param tunnel The tunnel to apply the configuration to.
- * @param config The new configuration for this tunnel.
- * @return The updated configuration of the tunnel.
- */
- Config applyConfig(Tunnel tunnel, Config config) throws Exception;
-
- /**
- * Enumerate the names of currently-running tunnels.
+ * Enumerate names of currently-running tunnels.
*
* @return The set of running tunnel names.
*/
- Set<String> enumerate();
+ Set<String> getRunningTunnelNames();
/**
- * Get the actual state of a tunnel.
+ * Get the state of a tunnel.
*
* @param tunnel The tunnel to examine the state of.
* @return The state of the tunnel.
+ * @throws Exception Exception raised when retrieving tunnel's state.
*/
- State getState(Tunnel tunnel) throws Exception;
+ Tunnel.State getState(Tunnel tunnel) throws Exception;
/**
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
@@ -49,31 +40,28 @@ public interface Backend {
*
* @param tunnel The tunnel to retrieve statistics for.
* @return The statistics for the tunnel.
+ * @throws Exception Exception raised when retrieving statistics.
*/
Statistics getStatistics(Tunnel tunnel) throws Exception;
/**
- * Determine type name of underlying backend.
- *
- * @return Type name
- */
- String getTypePrettyName();
-
- /**
* Determine version of underlying backend.
*
* @return The version of the backend.
- * @throws Exception
+ * @throws Exception Exception raised while retrieving version.
*/
String getVersion() throws Exception;
/**
- * Set the state of a tunnel.
+ * Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
+ * may update the running configuration; config may be null when setting the tunnel down.
*
* @param tunnel The tunnel to control the state of.
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
* {@code TOGGLE}.
+ * @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
* @return The updated state of the tunnel.
+ * @throws Exception Exception raised while changing state.
*/
- State setState(Tunnel tunnel, State state) throws Exception;
+ Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java b/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java
new file mode 100644
index 00000000..af966ec1
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import com.wireguard.util.NonNullForAll;
+
+/**
+ * A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
+ * implementations of {@link Backend}.
+ */
+@NonNullForAll
+public final class BackendException extends Exception {
+ private final Object[] format;
+ private final Reason reason;
+
+ /**
+ * Public constructor for BackendException.
+ *
+ * @param reason The {@link Reason} which caused this exception to be thrown
+ * @param format Format string values used when converting exceptions to user-facing strings.
+ */
+ public BackendException(final Reason reason, final Object... format) {
+ this.reason = reason;
+ this.format = format;
+ }
+
+ /**
+ * Get the format string values associated with the instance.
+ *
+ * @return Array of {@link Object} for string formatting purposes
+ */
+ public Object[] getFormat() {
+ return format;
+ }
+
+ /**
+ * Get the reason for this exception.
+ *
+ * @return Associated {@link Reason} for this exception.
+ */
+ public Reason getReason() {
+ return reason;
+ }
+
+ /**
+ * Enum class containing all known reasons for why a {@link BackendException} might be thrown.
+ */
+ public enum Reason {
+ UNKNOWN_KERNEL_MODULE_NAME,
+ WG_QUICK_CONFIG_ERROR_CODE,
+ TUNNEL_MISSING_CONFIG,
+ VPN_NOT_AUTHORIZED,
+ UNABLE_TO_START_VPN,
+ TUN_CREATION_ERROR,
+ GO_ACTIVATION_ERROR_CODE,
+ DNS_RESOLUTION_FAILURE,
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
new file mode 100644
index 00000000..429cb1f1
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.wireguard.android.backend.BackendException.Reason;
+import com.wireguard.android.backend.Tunnel.State;
+import com.wireguard.android.util.SharedLibraryLoader;
+import com.wireguard.config.Config;
+import com.wireguard.config.InetEndpoint;
+import com.wireguard.config.InetNetwork;
+import com.wireguard.config.Peer;
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.util.NonNullForAll;
+
+import java.net.InetAddress;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import androidx.annotation.Nullable;
+import androidx.collection.ArraySet;
+
+/**
+ * Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
+ * WireGuard tunnels.
+ */
+@NonNullForAll
+public final class GoBackend implements Backend {
+ private static final int DNS_RESOLUTION_RETRIES = 10;
+ private static final String TAG = "WireGuard/GoBackend";
+ @Nullable private static AlwaysOnCallback alwaysOnCallback;
+ private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
+ private final Context context;
+ @Nullable private Config currentConfig;
+ @Nullable private Tunnel currentTunnel;
+ private int currentTunnelHandle = -1;
+
+ /**
+ * Public constructor for GoBackend.
+ *
+ * @param context An Android {@link Context}
+ */
+ public GoBackend(final Context context) {
+ SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
+ this.context = context;
+ }
+
+ /**
+ * Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
+ * system's Always-On VPN mode.
+ *
+ * @param cb Callback to be invoked
+ */
+ public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
+ alwaysOnCallback = cb;
+ }
+
+ @Nullable private static native String wgGetConfig(int handle);
+
+ private static native int wgGetSocketV4(int handle);
+
+ private static native int wgGetSocketV6(int handle);
+
+ private static native void wgTurnOff(int handle);
+
+ private static native int wgTurnOn(String ifName, int tunFd, String settings);
+
+ private static native String wgVersion();
+
+ /**
+ * Method to get the names of running tunnels.
+ *
+ * @return A set of string values denoting names of running tunnels.
+ */
+ @Override
+ public Set<String> getRunningTunnelNames() {
+ if (currentTunnel != null) {
+ final Set<String> runningTunnels = new ArraySet<>();
+ runningTunnels.add(currentTunnel.getName());
+ return runningTunnels;
+ }
+ return Collections.emptySet();
+ }
+
+ /**
+ * Get the associated {@link State} for a given {@link Tunnel}.
+ *
+ * @param tunnel The tunnel to examine the state of.
+ * @return {@link State} associated with the given tunnel.
+ */
+ @Override
+ public State getState(final Tunnel tunnel) {
+ return currentTunnel == tunnel ? State.UP : State.DOWN;
+ }
+
+ /**
+ * Get the associated {@link Statistics} for a given {@link Tunnel}.
+ *
+ * @param tunnel The tunnel to retrieve statistics for.
+ * @return {@link Statistics} associated with the given tunnel.
+ */
+ @Override
+ public Statistics getStatistics(final Tunnel tunnel) {
+ final Statistics stats = new Statistics();
+ if (tunnel != currentTunnel || currentTunnelHandle == -1)
+ return stats;
+ final String config = wgGetConfig(currentTunnelHandle);
+ if (config == null)
+ return stats;
+ Key key = null;
+ long rx = 0;
+ long tx = 0;
+ long latestHandshakeMSec = 0;
+ for (final String line : config.split("\\n")) {
+ if (line.startsWith("public_key=")) {
+ if (key != null)
+ stats.add(key, rx, tx, latestHandshakeMSec);
+ rx = 0;
+ tx = 0;
+ latestHandshakeMSec = 0;
+ try {
+ key = Key.fromHex(line.substring(11));
+ } catch (final KeyFormatException ignored) {
+ key = null;
+ }
+ } else if (line.startsWith("rx_bytes=")) {
+ if (key == null)
+ continue;
+ try {
+ rx = Long.parseLong(line.substring(9));
+ } catch (final NumberFormatException ignored) {
+ rx = 0;
+ }
+ } else if (line.startsWith("tx_bytes=")) {
+ if (key == null)
+ continue;
+ try {
+ tx = Long.parseLong(line.substring(9));
+ } catch (final NumberFormatException ignored) {
+ tx = 0;
+ }
+ } else if (line.startsWith("last_handshake_time_sec=")) {
+ if (key == null)
+ continue;
+ try {
+ latestHandshakeMSec += Long.parseLong(line.substring(24)) * 1000;
+ } catch (final NumberFormatException ignored) {
+ latestHandshakeMSec = 0;
+ }
+ } else if (line.startsWith("last_handshake_time_nsec=")) {
+ if (key == null)
+ continue;
+ try {
+ latestHandshakeMSec += Long.parseLong(line.substring(25)) / 1000000;
+ } catch (final NumberFormatException ignored) {
+ latestHandshakeMSec = 0;
+ }
+ }
+ }
+ if (key != null)
+ stats.add(key, rx, tx, latestHandshakeMSec);
+ return stats;
+ }
+
+ /**
+ * Get the version of the underlying wireguard-go library.
+ *
+ * @return {@link String} value of the version of the wireguard-go library.
+ */
+ @Override
+ public String getVersion() {
+ return wgVersion();
+ }
+
+ /**
+ * Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
+ *
+ * @param tunnel The tunnel to control the state of.
+ * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
+ * {@code TOGGLE}.
+ * @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
+ * @return {@link State} of the tunnel after state changes are applied.
+ * @throws Exception Exception raised while changing tunnel state.
+ */
+ @Override
+ public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
+ final State originalState = getState(tunnel);
+
+ if (state == State.TOGGLE)
+ state = originalState == State.UP ? State.DOWN : State.UP;
+ if (state == originalState && tunnel == currentTunnel && config == currentConfig)
+ return originalState;
+ if (state == State.UP) {
+ final Config originalConfig = currentConfig;
+ final Tunnel originalTunnel = currentTunnel;
+ if (currentTunnel != null)
+ setStateInternal(currentTunnel, null, State.DOWN);
+ try {
+ setStateInternal(tunnel, config, state);
+ } catch (final Exception e) {
+ if (originalTunnel != null)
+ setStateInternal(originalTunnel, originalConfig, State.UP);
+ throw e;
+ }
+ } else if (state == State.DOWN && tunnel == currentTunnel) {
+ setStateInternal(tunnel, null, State.DOWN);
+ }
+ return getState(tunnel);
+ }
+
+ private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
+ throws Exception {
+ Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
+
+ if (state == State.UP) {
+ if (config == null)
+ throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
+
+ if (VpnService.prepare(context) != null)
+ throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
+
+ final VpnService service;
+ if (!vpnService.isDone()) {
+ Log.d(TAG, "Requesting to start VpnService");
+ context.startService(new Intent(context, VpnService.class));
+ }
+
+ try {
+ service = vpnService.get(2, TimeUnit.SECONDS);
+ } catch (final TimeoutException e) {
+ final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
+ be.initCause(e);
+ throw be;
+ }
+ service.setOwner(this);
+
+ if (currentTunnelHandle != -1) {
+ Log.w(TAG, "Tunnel already up");
+ return;
+ }
+
+
+ dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) {
+ // Pre-resolve IPs so they're cached when building the userspace string
+ for (final Peer peer : config.getPeers()) {
+ final InetEndpoint ep = peer.getEndpoint().orElse(null);
+ if (ep == null)
+ continue;
+ if (ep.getResolved().orElse(null) == null) {
+ if (i < DNS_RESOLUTION_RETRIES - 1) {
+ Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again");
+ Thread.sleep(1000);
+ continue dnsRetry;
+ } else
+ throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost());
+ }
+ }
+ break;
+ }
+
+ // Build config
+ final String goConfig = config.toWgUserspaceString();
+
+ // Create the vpn tunnel with android API
+ final VpnService.Builder builder = service.getBuilder();
+ builder.setSession(tunnel.getName());
+
+ for (final String excludedApplication : config.getInterface().getExcludedApplications())
+ builder.addDisallowedApplication(excludedApplication);
+
+ for (final String includedApplication : config.getInterface().getIncludedApplications())
+ builder.addAllowedApplication(includedApplication);
+
+ for (final InetNetwork addr : config.getInterface().getAddresses())
+ builder.addAddress(addr.getAddress(), addr.getMask());
+
+ for (final InetAddress addr : config.getInterface().getDnsServers())
+ builder.addDnsServer(addr.getHostAddress());
+
+ for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains())
+ builder.addSearchDomain(dnsSearchDomain);
+
+ boolean sawDefaultRoute = false;
+ for (final Peer peer : config.getPeers()) {
+ for (final InetNetwork addr : peer.getAllowedIps()) {
+ if (addr.getMask() == 0)
+ sawDefaultRoute = true;
+ builder.addRoute(addr.getAddress(), addr.getMask());
+ }
+ }
+
+ // "Kill-switch" semantics
+ if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
+ builder.allowFamily(OsConstants.AF_INET);
+ builder.allowFamily(OsConstants.AF_INET6);
+ }
+
+ builder.setMtu(config.getInterface().getMtu().orElse(1280));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ builder.setMetered(false);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ service.setUnderlyingNetworks(null);
+
+ builder.setBlocking(true);
+ try (final ParcelFileDescriptor tun = builder.establish()) {
+ if (tun == null)
+ throw new BackendException(Reason.TUN_CREATION_ERROR);
+ Log.d(TAG, "Go backend " + wgVersion());
+ currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
+ }
+ if (currentTunnelHandle < 0)
+ throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
+
+ currentTunnel = tunnel;
+ currentConfig = config;
+
+ service.protect(wgGetSocketV4(currentTunnelHandle));
+ service.protect(wgGetSocketV6(currentTunnelHandle));
+ } else {
+ if (currentTunnelHandle == -1) {
+ Log.w(TAG, "Tunnel already down");
+ return;
+ }
+ int handleToClose = currentTunnelHandle;
+ currentTunnel = null;
+ currentTunnelHandle = -1;
+ currentConfig = null;
+ wgTurnOff(handleToClose);
+ try {
+ vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf();
+ } catch (final TimeoutException ignored) { }
+ }
+
+ tunnel.onStateChange(state);
+ }
+
+ /**
+ * Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
+ * system's Always-On VPN mode.
+ */
+ public interface AlwaysOnCallback {
+ void alwaysOnTriggered();
+ }
+
+ // TODO: When we finally drop API 21 and move to API 24, delete this and replace with the ordinary CompletableFuture.
+ private static final class GhettoCompletableFuture<V> {
+ private final LinkedBlockingQueue<V> completion = new LinkedBlockingQueue<>(1);
+ private final FutureTask<V> result = new FutureTask<>(completion::peek);
+
+ public boolean complete(final V value) {
+ final boolean offered = completion.offer(value);
+ if (offered)
+ result.run();
+ return offered;
+ }
+
+ public V get() throws ExecutionException, InterruptedException {
+ return result.get();
+ }
+
+ public V get(final long timeout, final TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
+ return result.get(timeout, unit);
+ }
+
+ public boolean isDone() {
+ return !completion.isEmpty();
+ }
+
+ public GhettoCompletableFuture<V> newIncompleteFuture() {
+ return new GhettoCompletableFuture<>();
+ }
+ }
+
+ /**
+ * {@link android.net.VpnService} implementation for {@link GoBackend}
+ */
+ public static class VpnService extends android.net.VpnService {
+ @Nullable private GoBackend owner;
+
+ public Builder getBuilder() {
+ return new Builder();
+ }
+
+ @Override
+ public void onCreate() {
+ vpnService.complete(this);
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (owner != null) {
+ final Tunnel tunnel = owner.currentTunnel;
+ if (tunnel != null) {
+ if (owner.currentTunnelHandle != -1)
+ wgTurnOff(owner.currentTunnelHandle);
+ owner.currentTunnel = null;
+ owner.currentTunnelHandle = -1;
+ owner.currentConfig = null;
+ tunnel.onStateChange(State.DOWN);
+ }
+ }
+ vpnService = vpnService.newIncompleteFuture();
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
+ vpnService.complete(this);
+ if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
+ Log.d(TAG, "Service started by Always-on VPN feature");
+ if (alwaysOnCallback != null)
+ alwaysOnCallback.alwaysOnTriggered();
+ }
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ public void setOwner(final GoBackend owner) {
+ this.owner = owner;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
new file mode 100644
index 00000000..08b84949
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.os.SystemClock;
+
+import com.wireguard.crypto.Key;
+import com.wireguard.util.NonNullForAll;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Class representing transfer statistics for a {@link Tunnel} instance.
+ */
+@NonNullForAll
+public class Statistics {
+ public record PeerStats(long rxBytes, long txBytes, long latestHandshakeEpochMillis) { }
+ private final Map<Key, PeerStats> stats = new HashMap<>();
+ private long lastTouched = SystemClock.elapsedRealtime();
+
+ Statistics() {
+ }
+
+ /**
+ * Add a peer and its current stats to the internal map.
+ *
+ * @param key A WireGuard public key bound to a particular peer
+ * @param rxBytes The received traffic for the {@link com.wireguard.config.Peer} referenced by
+ * the provided {@link Key}. This value is in bytes
+ * @param txBytes The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
+ * the provided {@link Key}. This value is in bytes.
+ * @param latestHandshake The timestamp of the latest handshake for the {@link com.wireguard.config.Peer}
+ * referenced by the provided {@link Key}. The value is in epoch milliseconds.
+ */
+ void add(final Key key, final long rxBytes, final long txBytes, final long latestHandshake) {
+ stats.put(key, new PeerStats(rxBytes, txBytes, latestHandshake));
+ lastTouched = SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
+ *
+ * @return boolean indicating if the current statistics instance has stale values.
+ */
+ public boolean isStale() {
+ return SystemClock.elapsedRealtime() - lastTouched > 900;
+ }
+
+ /**
+ * Get the statistics for the {@link com.wireguard.config.Peer} referenced by the provided {@link Key}
+ *
+ * @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
+ * @return a {@link PeerStats} representing various statistics about this peer.
+ */
+ @Nullable
+ public PeerStats peer(final Key peer) {
+ return stats.get(peer);
+ }
+
+ /**
+ * Get the list of peers being tracked by this instance.
+ *
+ * @return An array of {@link Key} instances representing WireGuard
+ * {@link com.wireguard.config.Peer}s
+ */
+ public Key[] peers() {
+ return stats.keySet().toArray(new Key[0]);
+ }
+
+ /**
+ * Get the total received traffic by all the peers being tracked by this instance
+ *
+ * @return a long representing the number of bytes received by the peers being tracked.
+ */
+ public long totalRx() {
+ long rx = 0;
+ for (final PeerStats val : stats.values()) {
+ rx += val.rxBytes;
+ }
+ return rx;
+ }
+
+ /**
+ * Get the total transmitted traffic by all the peers being tracked by this instance
+ *
+ * @return a long representing the number of bytes transmitted by the peers being tracked.
+ */
+ public long totalTx() {
+ long tx = 0;
+ for (final PeerStats val : stats.values()) {
+ tx += val.txBytes;
+ }
+ return tx;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
new file mode 100644
index 00000000..766df443
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.util.regex.Pattern;
+
+/**
+ * Represents a WireGuard tunnel.
+ */
+
+@NonNullForAll
+public interface Tunnel {
+ int NAME_MAX_LENGTH = 15;
+ Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
+
+ static boolean isNameInvalid(final CharSequence name) {
+ return !NAME_PATTERN.matcher(name).matches();
+ }
+
+ /**
+ * Get the name of the tunnel, which should always pass the !isNameInvalid test.
+ *
+ * @return The name of the tunnel.
+ */
+ String getName();
+
+ /**
+ * React to a change in state of the tunnel. Should only be directly called by Backend.
+ *
+ * @param newState The new state of the tunnel.
+ */
+ void onStateChange(State newState);
+
+ /**
+ * Enum class to represent all possible states of a {@link Tunnel}.
+ */
+ enum State {
+ DOWN,
+ TOGGLE,
+ UP;
+
+ /**
+ * Get the state of a {@link Tunnel}
+ *
+ * @param running boolean indicating if the tunnel is running.
+ * @return State of the tunnel based on whether or not it is running.
+ */
+ public static State of(final boolean running) {
+ return running ? UP : DOWN;
+ }
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
new file mode 100644
index 00000000..023743a8
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.Pair;
+
+import com.wireguard.android.backend.BackendException.Reason;
+import com.wireguard.android.backend.Tunnel.State;
+import com.wireguard.android.util.RootShell;
+import com.wireguard.android.util.ToolsInstaller;
+import com.wireguard.config.Config;
+import com.wireguard.crypto.Key;
+import com.wireguard.util.NonNullForAll;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Implementation of {@link Backend} that uses the kernel module and {@code wg-quick} to provide
+ * WireGuard tunnels.
+ */
+
+@NonNullForAll
+public final class WgQuickBackend implements Backend {
+ private static final String TAG = "WireGuard/WgQuickBackend";
+ private final File localTemporaryDir;
+ private final RootShell rootShell;
+ private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
+ private final ToolsInstaller toolsInstaller;
+ private boolean multipleTunnels;
+
+ public WgQuickBackend(final Context context, final RootShell rootShell, final ToolsInstaller toolsInstaller) {
+ localTemporaryDir = new File(context.getCacheDir(), "tmp");
+ this.rootShell = rootShell;
+ this.toolsInstaller = toolsInstaller;
+ }
+
+ public static boolean hasKernelSupport() {
+ return new File("/sys/module/wireguard").exists();
+ }
+
+ @Override
+ public Set<String> getRunningTunnelNames() {
+ final List<String> output = new ArrayList<>();
+ // Don't throw an exception here or nothing will show up in the UI.
+ try {
+ toolsInstaller.ensureToolsAvailable();
+ if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
+ return Collections.emptySet();
+ } catch (final Exception e) {
+ Log.w(TAG, "Unable to enumerate running tunnels", e);
+ return Collections.emptySet();
+ }
+ // wg puts all interface names on the same line. Split them into separate elements.
+ return Set.of(output.get(0).split(" "));
+ }
+
+ @Override
+ public State getState(final Tunnel tunnel) {
+ return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN;
+ }
+
+ @Override
+ public Statistics getStatistics(final Tunnel tunnel) {
+ final Statistics stats = new Statistics();
+ final Collection<String> output = new ArrayList<>();
+ try {
+ if (rootShell.run(output, String.format("wg show '%s' dump", tunnel.getName())) != 0)
+ return stats;
+ } catch (final Exception ignored) {
+ return stats;
+ }
+ for (final String line : output) {
+ final String[] parts = line.split("\\t");
+ if (parts.length != 8)
+ continue;
+ try {
+ stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[5]), Long.parseLong(parts[6]), Long.parseLong(parts[4]) * 1000);
+ } catch (final Exception ignored) {
+ }
+ }
+ return stats;
+ }
+
+ @Override
+ public String getVersion() throws Exception {
+ final List<String> output = new ArrayList<>();
+ if (rootShell.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
+ throw new BackendException(Reason.UNKNOWN_KERNEL_MODULE_NAME);
+ return output.get(0);
+ }
+
+ public void setMultipleTunnels(final boolean on) {
+ multipleTunnels = on;
+ }
+
+ @Override
+ public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
+ final State originalState = getState(tunnel);
+ final Config originalConfig = runningConfigs.get(tunnel);
+ final Map<Tunnel, Config> runningConfigsSnapshot = new HashMap<>(runningConfigs);
+
+ if (state == State.TOGGLE)
+ state = originalState == State.UP ? State.DOWN : State.UP;
+ if ((state == State.UP && originalState == State.UP && originalConfig != null && originalConfig == config) ||
+ (state == State.DOWN && originalState == State.DOWN))
+ return originalState;
+ if (state == State.UP) {
+ toolsInstaller.ensureToolsAvailable();
+ if (!multipleTunnels && originalState == State.DOWN) {
+ final List<Pair<Tunnel, Config>> rewind = new LinkedList<>();
+ try {
+ for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
+ setStateInternal(entry.getKey(), entry.getValue(), State.DOWN);
+ rewind.add(Pair.create(entry.getKey(), entry.getValue()));
+ }
+ } catch (final Exception e) {
+ try {
+ for (final Pair<Tunnel, Config> entry : rewind) {
+ setStateInternal(entry.first, entry.second, State.UP);
+ }
+ } catch (final Exception ignored) {
+ }
+ throw e;
+ }
+ }
+ if (originalState == State.UP)
+ setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
+ try {
+ setStateInternal(tunnel, config, State.UP);
+ } catch (final Exception e) {
+ try {
+ if (originalState == State.UP && originalConfig != null) {
+ setStateInternal(tunnel, originalConfig, State.UP);
+ }
+ if (!multipleTunnels && originalState == State.DOWN) {
+ for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
+ setStateInternal(entry.getKey(), entry.getValue(), State.UP);
+ }
+ }
+ } catch (final Exception ignored) {
+ }
+ throw e;
+ }
+ } else if (state == State.DOWN) {
+ setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
+ }
+ return state;
+ }
+
+ private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
+ Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
+
+ Objects.requireNonNull(config, "Trying to set state up with a null config");
+
+ final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
+ try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
+ stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
+ }
+ String command = String.format("wg-quick %s '%s'",
+ state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
+ if (state == State.UP)
+ command = "cat /sys/module/wireguard/version && " + command;
+ final int result = rootShell.run(null, command);
+ // noinspection ResultOfMethodCallIgnored
+ tempFile.delete();
+ if (result != 0)
+ throw new BackendException(Reason.WG_QUICK_CONFIG_ERROR_CODE, result);
+
+ if (state == State.UP)
+ runningConfigs.put(tunnel, config);
+ else
+ runningConfigs.remove(tunnel);
+
+ tunnel.onStateChange(state);
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/util/RootShell.java b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
index 1fe5667a..67bff568 100644
--- a/app/src/main/java/com/wireguard/android/util/RootShell.java
+++ b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
@@ -1,20 +1,18 @@
/*
- * Copyright © 2017-2019 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.Context;
-import androidx.annotation.Nullable;
import android.util.Log;
-import com.wireguard.android.BuildConfig;
-import com.wireguard.android.R;
+import com.wireguard.android.util.RootShell.RootShellException.Reason;
+import com.wireguard.util.NonNullForAll;
import java.io.BufferedReader;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
@@ -22,16 +20,17 @@ import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.UUID;
+import androidx.annotation.Nullable;
+
/**
* Helper class for running commands as root.
*/
+@NonNullForAll
public class RootShell {
private static final String SU = "su";
- private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName();
+ private static final String TAG = "WireGuard/RootShell";
- private final Context context;
- private final String deviceNotRootedMessage;
private final File localBinaryDir;
private final File localTemporaryDir;
private final Object lock = new Object();
@@ -42,12 +41,13 @@ public class RootShell {
@Nullable private BufferedReader stdout;
public RootShell(final Context context) {
- deviceNotRootedMessage = context.getString(R.string.error_root);
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
localTemporaryDir = new File(context.getCacheDir(), "tmp");
- preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
- BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir);
- this.context = context;
+ final String packageName = context.getPackageName();
+ if (packageName.contains("'"))
+ throw new RuntimeException("Impossibly invalid package name contains a single quote");
+ preamble = String.format("export CALLING_PACKAGE='%s' PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE uid=%d\" >/dev/null 2>&1; id -u\n",
+ packageName, localBinaryDir, localTemporaryDir, android.os.Process.myUid());
}
private static boolean isExecutableInPath(final String name) {
@@ -83,7 +83,7 @@ public class RootShell {
* @return The exit value of the command.
*/
public int run(@Nullable final Collection<String> output, final String command)
- throws IOException, NoRootException {
+ throws IOException, RootShellException {
synchronized (lock) {
/* Start inside synchronized block to prevent a concurrent call to stop(). */
start();
@@ -122,24 +122,24 @@ public class RootShell {
}
}
if (markersSeen != 4)
- throw new IOException(context.getString(R.string.shell_marker_count_error, markersSeen));
+ throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
if (errnoStdout != errnoStderr)
- throw new IOException(context.getString(R.string.shell_exit_status_read_error));
+ throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
Log.v(TAG, "exit: " + errnoStdout);
return errnoStdout;
}
}
- public void start() throws IOException, NoRootException {
+ public void start() throws IOException, RootShellException {
if (!isExecutableInPath(SU))
- throw new NoRootException(deviceNotRootedMessage);
+ throw new RootShellException(Reason.NO_ROOT_ACCESS);
synchronized (lock) {
if (isRunning())
return;
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
- throw new FileNotFoundException(context.getString(R.string.create_bin_dir_error));
+ throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
- throw new FileNotFoundException(context.getString(R.string.create_temp_dir_error));
+ throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
try {
final ProcessBuilder builder = new ProcessBuilder().command(SU);
builder.environment().put("LC_ALL", "C");
@@ -147,7 +147,9 @@ public class RootShell {
process = builder.start();
} catch (final IOException e) {
// A failure at this stage means the device isn't rooted.
- throw new NoRootException(deviceNotRootedMessage, e);
+ final RootShellException rse = new RootShellException(Reason.NO_ROOT_ACCESS);
+ rse.initCause(e);
+ throw rse;
}
stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8);
stdout = new BufferedReader(new InputStreamReader(process.getInputStream(),
@@ -160,18 +162,18 @@ public class RootShell {
final String uid = stdout.readLine();
if (!"0".equals(uid)) {
Log.w(TAG, "Root check did not return correct UID: " + uid);
- throw new NoRootException(deviceNotRootedMessage);
+ throw new RootShellException(Reason.NO_ROOT_ACCESS);
}
if (!isRunning()) {
String line;
while ((line = stderr.readLine()) != null) {
Log.w(TAG, "Root check returned an error: " + line);
if (line.contains("Permission denied"))
- throw new NoRootException(deviceNotRootedMessage);
+ throw new RootShellException(Reason.NO_ROOT_ACCESS);
}
- throw new IOException(context.getString(R.string.shell_start_error, process.exitValue()));
+ throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
}
- } catch (final IOException | NoRootException e) {
+ } catch (final IOException | RootShellException e) {
stop();
throw e;
}
@@ -187,13 +189,34 @@ public class RootShell {
}
}
- public static class NoRootException extends Exception {
- public NoRootException(final String message, final Throwable cause) {
- super(message, cause);
+ public static class RootShellException extends Exception {
+ private final Object[] format;
+ private final Reason reason;
+
+ public RootShellException(final Reason reason, final Object... format) {
+ this.reason = reason;
+ this.format = format;
+ }
+
+ public Object[] getFormat() {
+ return format;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+
+ public boolean isIORelated() {
+ return reason != Reason.NO_ROOT_ACCESS;
}
- public NoRootException(final String message) {
- super(message);
+ public enum Reason {
+ NO_ROOT_ACCESS,
+ SHELL_MARKER_COUNT_ERROR,
+ SHELL_EXIT_STATUS_READ_ERROR,
+ SHELL_START_ERROR,
+ CREATE_BIN_DIR_ERROR,
+ CREATE_TEMP_DIR_ERROR
}
}
}
diff --git a/app/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java b/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
index e3923d19..15a00467 100644
--- a/app/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
+++ b/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
@@ -1,5 +1,5 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
@@ -9,6 +9,8 @@ import android.content.Context;
import android.os.Build;
import android.util.Log;
+import com.wireguard.util.NonNullForAll;
+
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -19,8 +21,13 @@ import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+@NonNullForAll
+@RestrictTo(Scope.LIBRARY_GROUP)
public final class SharedLibraryLoader {
- private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
+ private static final String TAG = "WireGuard/SharedLibraryLoader";
private SharedLibraryLoader() {
}
@@ -34,25 +41,21 @@ public final class SharedLibraryLoader {
for (final String abi : Build.SUPPORTED_ABIS) {
for (final String apk : apks) {
- final ZipFile zipFile;
- try {
- zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ);
- } catch (final IOException e) {
- throw new RuntimeException(e);
- }
-
- final String mappedLibName = System.mapLibraryName(libName);
- final byte[] buffer = new byte[1024 * 32];
- final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
- final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
- if (zipEntry == null)
- continue;
- Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
- try (final FileOutputStream out = new FileOutputStream(destination);
- final InputStream in = zipFile.getInputStream(zipEntry)) {
- int len;
- while ((len = in.read(buffer)) != -1) {
- out.write(buffer, 0, len);
+ try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
+ final String mappedLibName = System.mapLibraryName(libName);
+ final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
+ final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
+ if (zipEntry == null)
+ continue;
+ Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
+ try (final FileOutputStream out = new FileOutputStream(destination);
+ final InputStream in = zipFile.getInputStream(zipEntry)) {
+ int len;
+ final byte[] buffer = new byte[1024 * 32];
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ }
+ out.getFD().sync();
}
}
return true;
diff --git a/app/src/main/java/com/wireguard/android/util/ToolsInstaller.java b/tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java
index defdefd8..65d23eab 100644
--- a/app/src/main/java/com/wireguard/android/util/ToolsInstaller.java
+++ b/tunnel/src/main/java/com/wireguard/android/util/ToolsInstaller.java
@@ -1,19 +1,16 @@
/*
- * Copyright © 2017-2019 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.Context;
-import androidx.annotation.Nullable;
import android.system.OsConstants;
import android.util.Log;
-import com.wireguard.android.Application;
-import com.wireguard.android.BuildConfig;
-import com.wireguard.android.R;
-import com.wireguard.android.util.RootShell.NoRootException;
+import com.wireguard.android.util.RootShell.RootShellException;
+import com.wireguard.util.NonNullForAll;
import java.io.File;
import java.io.FileNotFoundException;
@@ -21,10 +18,15 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
/**
* Helper to install WireGuard tools to the system partition.
*/
+@NonNullForAll
public final class ToolsInstaller {
public static final int ERROR = 0x0;
public static final int MAGISK = 0x4;
@@ -37,17 +39,19 @@ public final class ToolsInstaller {
new File("/system/bin"),
};
@Nullable private static final File INSTALL_DIR = getInstallDir();
- private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
+ private static final String TAG = "WireGuard/ToolsInstaller";
private final Context context;
private final File localBinaryDir;
private final Object lock = new Object();
+ private final RootShell rootShell;
@Nullable private Boolean areToolsAvailable;
@Nullable private Boolean installAsMagiskModule;
- public ToolsInstaller(final Context context) {
+ public ToolsInstaller(final Context context, final RootShell rootShell) {
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
this.context = context;
+ this.rootShell = rootShell;
}
@Nullable
@@ -63,7 +67,7 @@ public final class ToolsInstaller {
return null;
}
- public int areInstalled() throws NoRootException {
+ public int areInstalled() throws RootShellException {
if (INSTALL_DIR == null)
return ERROR;
final StringBuilder script = new StringBuilder();
@@ -74,13 +78,17 @@ public final class ToolsInstaller {
}
script.append("exit ").append(OsConstants.EALREADY).append(';');
try {
- final int ret = Application.getRootShell().run(null, script.toString());
+ final int ret = rootShell.run(null, script.toString());
if (ret == OsConstants.EALREADY)
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
else
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
} catch (final IOException ignored) {
return ERROR;
+ } catch (final RootShellException e) {
+ if (e.isIORelated())
+ return ERROR;
+ throw e;
}
}
@@ -97,38 +105,67 @@ public final class ToolsInstaller {
}
}
if (!areToolsAvailable)
- throw new FileNotFoundException(
- context.getString(R.string.tools_unavailable_error));
+ throw new FileNotFoundException("Required tools unavailable");
}
}
- public int install() throws NoRootException, IOException {
+ public boolean extract() throws IOException {
+ localBinaryDir.mkdirs();
+ final File[] files = new File[EXECUTABLES.length];
+ final File[] tempFiles = new File[EXECUTABLES.length];
+ boolean allExist = true;
+ for (int i = 0; i < files.length; ++i) {
+ files[i] = new File(localBinaryDir, EXECUTABLES[i]);
+ tempFiles[i] = new File(localBinaryDir, EXECUTABLES[i] + ".tmp");
+ allExist &= files[i].exists();
+ }
+ if (allExist)
+ return false;
+ for (int i = 0; i < files.length; ++i) {
+ if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], tempFiles[i]))
+ throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
+ if (!tempFiles[i].setExecutable(true, false))
+ throw new IOException("Unable to mark " + tempFiles[i].getAbsolutePath() + " as executable");
+ if (!tempFiles[i].renameTo(files[i]))
+ throw new IOException("Unable to rename " + tempFiles[i].getAbsolutePath() + " to " + files[i].getAbsolutePath());
+ }
+ return true;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public int install() throws RootShellException, IOException {
+ if (!context.getPackageName().startsWith("com.wireguard."))
+ throw new SecurityException("The tools may only be installed system-wide from the main WireGuard app.");
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
}
- private int installMagisk() throws NoRootException, IOException {
+ private int installMagisk() throws RootShellException, IOException {
extract();
final StringBuilder script = new StringBuilder("set -ex; ");
- script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
- script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
- script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
- script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
+ script.append("trap 'rm -rf /data/adb/modules/wireguard' INT TERM EXIT; ");
+ script.append(String.format("rm -rf /data/adb/modules/wireguard/; mkdir -p /data/adb/modules/wireguard%s; ", INSTALL_DIR));
+ script.append("printf 'id=wireguard\nname=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /data/adb/modules/wireguard/module.prop; ");
+ script.append("touch /data/adb/modules/wireguard/auto_mount; ");
for (final String name : EXECUTABLES) {
- final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
+ final File destination = new File("/data/adb/modules/wireguard" + INSTALL_DIR, name);
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
new File(localBinaryDir, name), destination, destination, destination));
}
script.append("trap - INT TERM EXIT;");
try {
- return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
+ return rootShell.run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
} catch (final IOException ignored) {
return ERROR;
+ } catch (final RootShellException e) {
+ if (e.isIORelated())
+ return ERROR;
+ throw e;
}
}
- private int installSystem() throws NoRootException, IOException {
+ private int installSystem() throws RootShellException, IOException {
if (INSTALL_DIR == null)
return OsConstants.ENOENT;
extract();
@@ -140,36 +177,21 @@ public final class ToolsInstaller {
new File(localBinaryDir, name), destination, destination, destination));
}
try {
- return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
+ return rootShell.run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
} catch (final IOException ignored) {
return ERROR;
+ } catch (final RootShellException e) {
+ if (e.isIORelated())
+ return ERROR;
+ throw e;
}
}
- public boolean extract() throws IOException {
- localBinaryDir.mkdirs();
- final File files[] = new File[EXECUTABLES.length];
- boolean allExist = true;
- for (int i = 0; i < files.length; ++i) {
- files[i] = new File(localBinaryDir, EXECUTABLES[i]);
- allExist &= files[i].exists();
- }
- if (allExist)
- return false;
- for (int i = 0; i < files.length; ++i) {
- if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], files[i]))
- throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
- if (!files[i].setExecutable(true, false))
- throw new IOException("Unable to mark " + files[i].getAbsolutePath() + " as executable");
- }
- return true;
- }
-
private boolean willInstallAsMagiskModule() {
synchronized (lock) {
if (installAsMagiskModule == null) {
try {
- installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
+ installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
} catch (final Exception ignored) {
installAsMagiskModule = false;
}
diff --git a/app/src/main/java/com/wireguard/config/Attribute.java b/tunnel/src/main/java/com/wireguard/config/Attribute.java
index 375acc42..1b7bc368 100644
--- a/app/src/main/java/com/wireguard/config/Attribute.java
+++ b/tunnel/src/main/java/com/wireguard/config/Attribute.java
@@ -1,17 +1,18 @@
/*
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
-import android.text.TextUtils;
+import com.wireguard.util.NonNullForAll;
+import java.util.Iterator;
+import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java9.util.Optional;
-
+@NonNullForAll
public final class Attribute {
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
@@ -25,7 +26,17 @@ public final class Attribute {
}
public static String join(final Iterable<?> values) {
- return TextUtils.join(", ", values);
+ final Iterator<?> it = values.iterator();
+ if (!it.hasNext()) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ sb.append(it.next());
+ while (it.hasNext()) {
+ sb.append(", ");
+ sb.append(it.next());
+ }
+ return sb.toString();
}
public static Optional<Attribute> parse(final CharSequence line) {
diff --git a/app/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
index 6d41b065..db022e14 100644
--- a/app/src/main/java/com/wireguard/config/BadConfigException.java
+++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
@@ -1,14 +1,16 @@
/*
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
-import androidx.annotation.Nullable;
-
import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.util.NonNullForAll;
+
+import androidx.annotation.Nullable;
+@NonNullForAll
public class BadConfigException extends Exception {
private final Location location;
private final Reason reason;
@@ -70,6 +72,7 @@ public class BadConfigException extends Exception {
DNS("DNS"),
ENDPOINT("Endpoint"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
+ INCLUDED_APPLICATIONS("IncludedApplications"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
@@ -94,7 +97,6 @@ public class BadConfigException extends Exception {
INVALID_VALUE,
MISSING_ATTRIBUTE,
MISSING_SECTION,
- MISSING_VALUE,
SYNTAX_ERROR,
UNKNOWN_ATTRIBUTE,
UNKNOWN_SECTION
diff --git a/app/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java
index 62651b08..ee9cebce 100644
--- a/app/src/main/java/com/wireguard/config/Config.java
+++ b/tunnel/src/main/java/com/wireguard/config/Config.java
@@ -1,15 +1,14 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
-import androidx.annotation.Nullable;
-
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
+import com.wireguard.util.NonNullForAll;
import java.io.BufferedReader;
import java.io.IOException;
@@ -18,10 +17,10 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
+
+import androidx.annotation.Nullable;
/**
* Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
@@ -29,6 +28,7 @@ import java.util.Set;
* <p>
* Instances of this class are immutable.
*/
+@NonNullForAll
public final class Config {
private final Interface interfaze;
private final List<Peer> peers;
@@ -67,6 +67,7 @@ public final class Config {
final Collection<String> peerLines = new ArrayList<>();
boolean inInterfaceSection = false;
boolean inPeerSection = false;
+ boolean seenInterfaceSection = false;
@Nullable String line;
while ((line = reader.readLine()) != null) {
final int commentIndex = line.indexOf('#');
@@ -84,6 +85,7 @@ public final class Config {
if ("[Interface]".equalsIgnoreCase(line)) {
inInterfaceSection = true;
inPeerSection = false;
+ seenInterfaceSection = true;
} else if ("[Peer]".equalsIgnoreCase(line)) {
inInterfaceSection = false;
inPeerSection = true;
@@ -102,7 +104,7 @@ public final class Config {
}
if (inPeerSection)
builder.parsePeer(peerLines);
- else if (!inInterfaceSection)
+ if (!seenInterfaceSection)
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
Reason.MISSING_SECTION, null);
// Combine all [Interface] sections in the file.
@@ -183,7 +185,7 @@ public final class Config {
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// Defaults to an empty set.
- private final Set<Peer> peers = new LinkedHashSet<>();
+ private final ArrayList<Peer> peers = new ArrayList<>();
// No default; must be provided before building.
@Nullable private Interface interfaze;
diff --git a/tunnel/src/main/java/com/wireguard/config/InetAddresses.java b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java
new file mode 100644
index 00000000..e4d697c5
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/InetAddresses.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.util.NonNullForAll;
+
+import java.lang.reflect.Method;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Pattern;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utility methods for creating instances of {@link InetAddress}.
+ */
+@NonNullForAll
+public final class InetAddresses {
+ @Nullable private static final Method PARSER_METHOD;
+ private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
+ private static final Pattern VALID_HOSTNAME = Pattern.compile("^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?$");
+
+ static {
+ Method m = null;
+ try {
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
+ // noinspection JavaReflectionMemberAccess
+ m = InetAddress.class.getMethod("parseNumericAddress", String.class);
+ } catch (final Exception ignored) {
+ }
+ PARSER_METHOD = m;
+ }
+
+ private InetAddresses() {
+ }
+
+ /**
+ * Determines whether input is a valid DNS hostname.
+ *
+ * @param maybeHostname a string that is possibly a DNS hostname
+ * @return whether or not maybeHostname is a valid DNS hostname
+ */
+ public static boolean isHostname(final CharSequence maybeHostname) {
+ return VALID_HOSTNAME.matcher(maybeHostname).matches();
+ }
+
+ /**
+ * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
+ *
+ * @param address a string representing the IP address
+ * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
+ */
+ public static InetAddress parse(final String address) throws ParseException {
+ if (address.isEmpty())
+ throw new ParseException(InetAddress.class, address, "Empty address");
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
+ return android.net.InetAddresses.parseNumericAddress(address);
+ else if (PARSER_METHOD != null)
+ return (InetAddress) PARSER_METHOD.invoke(null, address);
+ else
+ throw new NoSuchMethodException("parseNumericAddress");
+ } catch (final IllegalArgumentException e) {
+ throw new ParseException(InetAddress.class, address, e);
+ } catch (final Exception e) {
+ final Throwable cause = e.getCause();
+ // Re-throw parsing exceptions with the original type, as callers might try to catch
+ // them. On the other hand, callers cannot be expected to handle reflection failures.
+ if (cause instanceof IllegalArgumentException)
+ throw new ParseException(InetAddress.class, address, cause);
+ try {
+ if (WONT_TOUCH_RESOLVER.matcher(address).matches())
+ return InetAddress.getByName(address);
+ else
+ throw new ParseException(InetAddress.class, address, "Not an IP address");
+ } catch (final UnknownHostException f) {
+ throw new ParseException(InetAddress.class, address, f);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
index a442258e..d1db432b 100644
--- a/app/src/main/java/com/wireguard/config/InetEndpoint.java
+++ b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
@@ -1,23 +1,23 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
-import androidx.annotation.Nullable;
-
-import org.threeten.bp.Duration;
-import org.threeten.bp.Instant;
+import com.wireguard.util.NonNullForAll;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
import java.util.regex.Pattern;
-import java9.util.Optional;
+import androidx.annotation.Nullable;
/**
@@ -25,6 +25,7 @@ import java9.util.Optional;
* <p>
* Instances of this class are externally immutable.
*/
+@NonNullForAll
public final class InetEndpoint {
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
@@ -49,7 +50,7 @@ public final class InetEndpoint {
try {
uri = new URI("wg://" + endpoint);
} catch (final URISyntaxException e) {
- throw new IllegalArgumentException(e);
+ throw new ParseException(InetEndpoint.class, endpoint, e);
}
if (uri.getPort() < 0 || uri.getPort() > 65535)
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
diff --git a/app/src/main/java/com/wireguard/config/InetNetwork.java b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
index f89322fd..4a918044 100644
--- a/app/src/main/java/com/wireguard/config/InetNetwork.java
+++ b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
@@ -1,10 +1,12 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
+import com.wireguard.util.NonNullForAll;
+
import java.net.Inet4Address;
import java.net.InetAddress;
@@ -13,6 +15,7 @@ import java.net.InetAddress;
* <p>
* Instances of this class are immutable.
*/
+@NonNullForAll
public final class InetNetwork {
private final InetAddress address;
private final int mask;
@@ -44,7 +47,7 @@ public final class InetNetwork {
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
if (rawMask > maxMask)
throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
- final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
+ final int mask = rawMask >= 0 ? rawMask : maxMask;
return new InetNetwork(address, mask);
}
diff --git a/app/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java
index 54944424..bebca2e5 100644
--- a/app/src/main/java/com/wireguard/config/Interface.java
+++ b/tunnel/src/main/java/com/wireguard/config/Interface.java
@@ -1,18 +1,17 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
-import androidx.annotation.Nullable;
-
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.crypto.KeyPair;
+import com.wireguard.util.NonNullForAll;
import java.net.InetAddress;
import java.util.Collection;
@@ -21,12 +20,11 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
-import java9.util.Lists;
-import java9.util.Optional;
-import java9.util.stream.Collectors;
-import java9.util.stream.StreamSupport;
+import androidx.annotation.Nullable;
/**
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
@@ -35,13 +33,16 @@ import java9.util.stream.StreamSupport;
* <p>
* Instances of this class are immutable.
*/
+@NonNullForAll
public final class Interface {
private static final int MAX_UDP_PORT = 65535;
private static final int MIN_UDP_PORT = 0;
private final Set<InetNetwork> addresses;
private final Set<InetAddress> dnsServers;
+ private final Set<String> dnsSearchDomains;
private final Set<String> excludedApplications;
+ private final Set<String> includedApplications;
private final KeyPair keyPair;
private final Optional<Integer> listenPort;
private final Optional<Integer> mtu;
@@ -50,7 +51,9 @@ public final class Interface {
// Defensively copy to ensure immutability even if the Builder is reused.
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
+ dnsSearchDomains = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsSearchDomains));
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
+ includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
listenPort = builder.listenPort;
mtu = builder.mtu;
@@ -80,6 +83,9 @@ public final class Interface {
case "excludedapplications":
builder.parseExcludedApplications(attribute.getValue());
break;
+ case "includedapplications":
+ builder.parseIncludedApplications(attribute.getValue());
+ break;
case "listenport":
builder.parseListenPort(attribute.getValue());
break;
@@ -104,7 +110,9 @@ public final class Interface {
final Interface other = (Interface) obj;
return addresses.equals(other.addresses)
&& dnsServers.equals(other.dnsServers)
+ && dnsSearchDomains.equals(other.dnsSearchDomains)
&& excludedApplications.equals(other.excludedApplications)
+ && includedApplications.equals(other.includedApplications)
&& keyPair.equals(other.keyPair)
&& listenPort.equals(other.listenPort)
&& mtu.equals(other.mtu);
@@ -131,6 +139,16 @@ public final class Interface {
}
/**
+ * Returns the set of DNS search domains associated with the interface.
+ *
+ * @return a set of strings
+ */
+ public Set<String> getDnsSearchDomains() {
+ // The collection is already immutable.
+ return dnsSearchDomains;
+ }
+
+ /**
* Returns the set of applications excluded from using the interface.
*
* @return a set of package names
@@ -141,6 +159,16 @@ public final class Interface {
}
/**
+ * Returns the set of applications included exclusively for using the interface.
+ *
+ * @return a set of package names
+ */
+ public Set<String> getIncludedApplications() {
+ // The collection is already immutable.
+ return includedApplications;
+ }
+
+ /**
* Returns the public/private key pair used by the interface.
*
* @return a key pair
@@ -173,6 +201,7 @@ public final class Interface {
hash = 31 * hash + addresses.hashCode();
hash = 31 * hash + dnsServers.hashCode();
hash = 31 * hash + excludedApplications.hashCode();
+ hash = 31 * hash + includedApplications.hashCode();
hash = 31 * hash + keyPair.hashCode();
hash = 31 * hash + listenPort.hashCode();
hash = 31 * hash + mtu.hashCode();
@@ -205,13 +234,14 @@ public final class Interface {
if (!addresses.isEmpty())
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
if (!dnsServers.isEmpty()) {
- final List<String> dnsServerStrings = StreamSupport.stream(dnsServers)
- .map(InetAddress::getHostAddress)
- .collect(Collectors.toUnmodifiableList());
+ final List<String> dnsServerStrings = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
+ dnsServerStrings.addAll(dnsSearchDomains);
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
}
if (!excludedApplications.isEmpty())
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
+ if (!includedApplications.isEmpty())
+ sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
@@ -238,7 +268,11 @@ public final class Interface {
// Defaults to an empty set.
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
// Defaults to an empty set.
+ private final Set<String> dnsSearchDomains = new LinkedHashSet<>();
+ // Defaults to an empty set.
private final Set<String> excludedApplications = new LinkedHashSet<>();
+ // Defaults to an empty set.
+ private final Set<String> includedApplications = new LinkedHashSet<>();
// No default; must be provided before building.
@Nullable private KeyPair keyPair;
// Defaults to not present.
@@ -266,10 +300,23 @@ public final class Interface {
return this;
}
+ public Builder addDnsSearchDomain(final String dnsSearchDomain) {
+ dnsSearchDomains.add(dnsSearchDomain);
+ return this;
+ }
+
+ public Builder addDnsSearchDomains(final Collection<String> dnsSearchDomains) {
+ this.dnsSearchDomains.addAll(dnsSearchDomains);
+ return this;
+ }
+
public Interface build() throws BadConfigException {
if (keyPair == null)
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
Reason.MISSING_ATTRIBUTE, null);
+ if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
+ throw new BadConfigException(Section.INTERFACE, Location.INCLUDED_APPLICATIONS,
+ Reason.INVALID_KEY, null);
return new Interface(this);
}
@@ -283,6 +330,16 @@ public final class Interface {
return this;
}
+ public Builder includeApplication(final String application) {
+ includedApplications.add(application);
+ return this;
+ }
+
+ public Builder includeApplications(final Collection<String> applications) {
+ includedApplications.addAll(applications);
+ return this;
+ }
+
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
try {
for (final String address : Attribute.split(addresses))
@@ -295,8 +352,15 @@ public final class Interface {
public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
try {
- for (final String dnsServer : Attribute.split(dnsServers))
- addDnsServer(InetAddresses.parse(dnsServer));
+ for (final String dnsServer : Attribute.split(dnsServers)) {
+ try {
+ addDnsServer(InetAddresses.parse(dnsServer));
+ } catch (final ParseException e) {
+ if (e.getParsingClass() != InetAddress.class || !InetAddresses.isHostname(dnsServer))
+ throw e;
+ addDnsSearchDomain(dnsServer);
+ }
+ }
return this;
} catch (final ParseException e) {
throw new BadConfigException(Section.INTERFACE, Location.DNS, e);
@@ -304,7 +368,11 @@ public final class Interface {
}
public Builder parseExcludedApplications(final CharSequence apps) {
- return excludeApplications(Lists.of(Attribute.split(apps)));
+ return excludeApplications(List.of(Attribute.split(apps)));
+ }
+
+ public Builder parseIncludedApplications(final CharSequence apps) {
+ return includeApplications(List.of(Attribute.split(apps)));
}
public Builder parseListenPort(final String listenPort) throws BadConfigException {
diff --git a/app/src/main/java/com/wireguard/config/ParseException.java b/tunnel/src/main/java/com/wireguard/config/ParseException.java
index c79d1fa1..e72ef3d7 100644
--- a/app/src/main/java/com/wireguard/config/ParseException.java
+++ b/tunnel/src/main/java/com/wireguard/config/ParseException.java
@@ -1,14 +1,18 @@
/*
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
+import com.wireguard.util.NonNullForAll;
+
import androidx.annotation.Nullable;
/**
+ *
*/
+@NonNullForAll
public class ParseException extends Exception {
private final Class<?> parsingClass;
private final CharSequence text;
diff --git a/app/src/main/java/com/wireguard/config/Peer.java b/tunnel/src/main/java/com/wireguard/config/Peer.java
index 37fcfa69..8a0fd763 100644
--- a/app/src/main/java/com/wireguard/config/Peer.java
+++ b/tunnel/src/main/java/com/wireguard/config/Peer.java
@@ -1,26 +1,26 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.config;
-import androidx.annotation.Nullable;
-
import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
+import com.wireguard.util.NonNullForAll;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
-import java9.util.Optional;
+import androidx.annotation.Nullable;
/**
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
@@ -28,6 +28,7 @@ import java9.util.Optional;
* <p>
* Instances of this class are immutable.
*/
+@NonNullForAll
public final class Peer {
private final Set<InetNetwork> allowedIps;
private final Optional<InetEndpoint> endpoint;
diff --git a/app/src/main/java/com/wireguard/crypto/Curve25519.java b/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
index 5622fc5f..aa66d1ca 100644
--- a/app/src/main/java/com/wireguard/crypto/Curve25519.java
+++ b/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
@@ -1,17 +1,19 @@
/*
* Copyright © 2016 Southern Storm Software, Pty Ltd.
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
-import androidx.annotation.Nullable;
+import com.wireguard.util.NonNullForAll;
import java.util.Arrays;
+import androidx.annotation.Nullable;
+
/**
- * Implementation of the Curve25519 elliptic curve algorithm.
+ * Implementation of Curve25519 ECDH.
* <p>
* This implementation was imported to WireGuard from noise-java:
* https://github.com/rweather/noise-java
@@ -25,6 +27,7 @@ import java.util.Arrays;
* References: http://cr.yp.to/ecdh.html, RFC 7748
*/
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
+@NonNullForAll
public final class Curve25519 {
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
private static final int NUM_LIMBS_255BIT = 10;
diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/tunnel/src/main/java/com/wireguard/crypto/Key.java
index f743ddd2..5fa93a46 100644
--- a/app/src/main/java/com/wireguard/crypto/Key.java
+++ b/tunnel/src/main/java/com/wireguard/crypto/Key.java
@@ -1,12 +1,14 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
import com.wireguard.crypto.KeyFormatException.Type;
+import com.wireguard.util.NonNullForAll;
+import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
@@ -17,6 +19,7 @@ import java.util.Arrays;
* Instances of this class are immutable.
*/
@SuppressWarnings("MagicNumber")
+@NonNullForAll
public final class Key {
private final byte[] key;
@@ -201,6 +204,16 @@ public final class Key {
return new Key(publicKey);
}
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this)
+ return true;
+ if (obj == null || obj.getClass() != getClass())
+ return false;
+ final Key other = (Key) obj;
+ return MessageDigest.isEqual(key, other.key);
+ }
+
/**
* Returns the key as an array of bytes.
*
@@ -211,6 +224,14 @@ public final class Key {
return Arrays.copyOf(key, key.length);
}
+ @Override
+ public int hashCode() {
+ int ret = 0;
+ for (int i = 0; i < key.length / 4; ++i)
+ ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
+ return ret;
+ }
+
/**
* Encodes the key to base64.
*
diff --git a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java b/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java
index 5818b4d4..c64b3dc8 100644
--- a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java
+++ b/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java
@@ -1,15 +1,18 @@
/*
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
+import com.wireguard.util.NonNullForAll;
+
/**
* An exception thrown when attempting to parse an invalid key (too short, too long, or byte
* data inappropriate for the format). The format being parsed can be accessed with the
* {@link #getFormat} method.
*/
+@NonNullForAll
public final class KeyFormatException extends Exception {
private final Key.Format format;
private final Type type;
diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java
index f8238e91..7a564689 100644
--- a/app/src/main/java/com/wireguard/crypto/KeyPair.java
+++ b/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java
@@ -1,15 +1,18 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.crypto;
+import com.wireguard.util.NonNullForAll;
+
/**
* Represents a Curve25519 key pair as used by WireGuard.
* <p>
* Instances of this class are immutable.
*/
+@NonNullForAll
public class KeyPair {
private final Key privateKey;
private final Key publicKey;
diff --git a/app/src/main/java/com/wireguard/util/NonNullForAll.java b/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
index f179fa49..1c395621 100644
--- a/app/src/main/java/com/wireguard/util/NonNullForAll.java
+++ b/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
@@ -1,11 +1,10 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.util;
-import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -13,14 +12,18 @@ import java.lang.annotation.RetentionPolicy;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
/**
* This annotation can be applied to a package, class or method to indicate that all
* class fields and method parameters and return values in that element are nonnull
* by default unless overridden.
*/
-@Documented
+@RestrictTo(Scope.LIBRARY_GROUP)
@Nonnull
@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
+
public @interface NonNullForAll {
}
diff --git a/tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java b/tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java
new file mode 100644
index 00000000..a24e049e
--- /dev/null
+++ b/tunnel/src/test/java/com/wireguard/config/BadConfigExceptionTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Reason;
+import com.wireguard.config.BadConfigException.Section;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class BadConfigExceptionTest {
+ private static final Map<String, InputStream> CONFIG_MAP = new HashMap<>();
+ private static final String[] CONFIG_NAMES = {
+ "invalid-key",
+ "invalid-number",
+ "invalid-value",
+ "missing-attribute",
+ "missing-section",
+ "syntax-error",
+ "unknown-attribute",
+ "unknown-section"
+ };
+
+ @AfterClass
+ public static void closeStreams() {
+ for (final InputStream inputStream : CONFIG_MAP.values()) {
+ try {
+ inputStream.close();
+ } catch (final IOException ignored) {
+ }
+ }
+ }
+
+ @BeforeClass
+ public static void readConfigs() {
+ for (final String config : CONFIG_NAMES) {
+ CONFIG_MAP.put(config, BadConfigExceptionTest.class.getClassLoader().getResourceAsStream(config + ".conf"));
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_INVALID_KEY_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("invalid-key"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.INVALID_KEY);
+ assertEquals(e.getLocation(), Location.PUBLIC_KEY);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_INVALID_NUMBER_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("invalid-number"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.INVALID_NUMBER);
+ assertEquals(e.getLocation(), Location.PERSISTENT_KEEPALIVE);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_INVALID_VALUE_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("invalid-value"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.INVALID_VALUE);
+ assertEquals(e.getLocation(), Location.DNS);
+ assertEquals(e.getSection(), Section.INTERFACE);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_MISSING_ATTRIBUTE_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("missing-attribute"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.MISSING_ATTRIBUTE);
+ assertEquals(e.getLocation(), Location.PUBLIC_KEY);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_MISSING_SECTION_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("missing-section"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.MISSING_SECTION);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.CONFIG);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_SYNTAX_ERROR_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("syntax-error"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.SYNTAX_ERROR);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_UNKNOWN_ATTRIBUTE_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("unknown-attribute"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.UNKNOWN_ATTRIBUTE);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.PEER);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+
+ @Test
+ public void throws_correctly_with_UNKNOWN_SECTION_reason() {
+ try {
+ Config.parse(CONFIG_MAP.get("unknown-section"));
+ fail("Config parsing must fail in this test");
+ } catch (final BadConfigException e) {
+ assertEquals(e.getReason(), Reason.UNKNOWN_SECTION);
+ assertEquals(e.getLocation(), Location.TOP_LEVEL);
+ assertEquals(e.getSection(), Section.CONFIG);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ fail("IOException throwing during test");
+ }
+ }
+}
diff --git a/tunnel/src/test/java/com/wireguard/config/ConfigTest.java b/tunnel/src/test/java/com/wireguard/config/ConfigTest.java
new file mode 100644
index 00000000..92143e72
--- /dev/null
+++ b/tunnel/src/test/java/com/wireguard/config/ConfigTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ConfigTest {
+
+ @Test(expected = BadConfigException.class)
+ public void invalid_config_throws() throws IOException, BadConfigException {
+ try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
+ Config.parse(is);
+ }
+ }
+
+ @Test
+ public void valid_config_parses_correctly() throws IOException, ParseException {
+ Config config = null;
+ final Collection<InetNetwork> expectedAllowedIps = new HashSet<>(Arrays.asList(InetNetwork.parse("0.0.0.0/0"), InetNetwork.parse("::0/0")));
+ try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("working.conf")) {
+ config = Config.parse(is);
+ } catch (final BadConfigException e) {
+ fail("'working.conf' should never fail to parse");
+ }
+ assertNotNull("config cannot be null after parsing", config);
+ assertTrue(
+ "No applications should be excluded by default",
+ config.getInterface().getExcludedApplications().isEmpty()
+ );
+ assertEquals("Test config has exactly one peer", 1, config.getPeers().size());
+ assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
+ assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
+ }
+}
diff --git a/tunnel/src/test/resources/broken.conf b/tunnel/src/test/resources/broken.conf
new file mode 100644
index 00000000..753c9717
--- /dev/null
+++ b/tunnel/src/test/resources/broken.conf
@@ -0,0 +1,9 @@
+[Interface]
+PrivateKey = l0lth1s1sd3f1n1t3lybr0k3n=
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+
+[Peer]
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
+AllowedIPs = 0.0.0.0/0,::0/0
+Endpoint = 192.0.2.1:51820
diff --git a/tunnel/src/test/resources/invalid-key.conf b/tunnel/src/test/resources/invalid-key.conf
new file mode 100644
index 00000000..215bec3b
--- /dev/null
+++ b/tunnel/src/test/resources/invalid-key.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6Og=
diff --git a/tunnel/src/test/resources/invalid-number.conf b/tunnel/src/test/resources/invalid-number.conf
new file mode 100644
index 00000000..f05fe32b
--- /dev/null
+++ b/tunnel/src/test/resources/invalid-number.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0L
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/invalid-value.conf b/tunnel/src/test/resources/invalid-value.conf
new file mode 100644
index 00000000..6a1e3b62
--- /dev/null
+++ b/tunnel/src/test/resources/invalid-value.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0,invalid_value
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/missing-attribute.conf b/tunnel/src/test/resources/missing-attribute.conf
new file mode 100644
index 00000000..ddf8cbb5
--- /dev/null
+++ b/tunnel/src/test/resources/missing-attribute.conf
@@ -0,0 +1,8 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
diff --git a/tunnel/src/test/resources/missing-section.conf b/tunnel/src/test/resources/missing-section.conf
new file mode 100644
index 00000000..676199ac
--- /dev/null
+++ b/tunnel/src/test/resources/missing-section.conf
@@ -0,0 +1,5 @@
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/syntax-error.conf b/tunnel/src/test/resources/syntax-error.conf
new file mode 100644
index 00000000..38b8ec95
--- /dev/null
+++ b/tunnel/src/test/resources/syntax-error.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint =
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/unknown-attribute.conf b/tunnel/src/test/resources/unknown-attribute.conf
new file mode 100644
index 00000000..f311161d
--- /dev/null
+++ b/tunnel/src/test/resources/unknown-attribute.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+DontLetTheFeelingFade = 1
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/unknown-section.conf b/tunnel/src/test/resources/unknown-section.conf
new file mode 100644
index 00000000..579d9717
--- /dev/null
+++ b/tunnel/src/test/resources/unknown-section.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peers]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/src/test/resources/working.conf b/tunnel/src/test/resources/working.conf
new file mode 100644
index 00000000..3f9665c3
--- /dev/null
+++ b/tunnel/src/test/resources/working.conf
@@ -0,0 +1,9 @@
+[Interface]
+Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
+DNS = 192.0.2.0
+PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
+[Peer]
+AllowedIPs = 0.0.0.0/0, ::0/0
+Endpoint = 192.0.2.1:51820
+PersistentKeepalive = 0
+PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
diff --git a/tunnel/tools/CMakeLists.txt b/tunnel/tools/CMakeLists.txt
new file mode 100644
index 00000000..490a1755
--- /dev/null
+++ b/tunnel/tools/CMakeLists.txt
@@ -0,0 +1,44 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+
+cmake_minimum_required(VERSION 3.4.1)
+project("WireGuard")
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
+add_link_options(LINKER:--build-id=none)
+add_compile_options(-Wall -Werror)
+
+add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
+target_compile_options(libwg-quick.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
+target_link_libraries(libwg-quick.so -ldl)
+
+file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
+add_executable(libwg.so ${WG_SOURCES})
+target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
+target_compile_options(libwg.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
+
+add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND "${ANDROID_HOST_PREBUILTS}/bin/make"
+ ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
+ ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
+ GRADLE_USER_HOME=${GRADLE_USER_HOME}
+ CC=${CMAKE_C_COMPILER}
+ CFLAGS=${CMAKE_C_FLAGS}
+ LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}
+ SYSROOT=${CMAKE_SYSROOT}
+ TARGET=${CMAKE_C_COMPILER_TARGET}
+ DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
+ BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
+)
+
+# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
+file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
+add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
+ -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
+ -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
+)
+add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
+ --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg.so>")
+add_dependencies(libwg.so elf-cleaner)
+add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
+ --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg-quick.so>")
+add_dependencies(libwg-quick.so elf-cleaner)
diff --git a/tunnel/tools/elf-cleaner b/tunnel/tools/elf-cleaner
new file mode 160000
+Subproject 7efc05090675ec6161b7def862728086a26c3b1
diff --git a/app/tools/libwg-go/.gitignore b/tunnel/tools/libwg-go/.gitignore
index d1638636..d1638636 100644
--- a/app/tools/libwg-go/.gitignore
+++ b/tunnel/tools/libwg-go/.gitignore
diff --git a/tunnel/tools/libwg-go/Makefile b/tunnel/tools/libwg-go/Makefile
new file mode 100644
index 00000000..427bfdc3
--- /dev/null
+++ b/tunnel/tools/libwg-go/Makefile
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+
+BUILDDIR ?= $(CURDIR)/build
+DESTDIR ?= $(CURDIR)/out
+
+NDK_GO_ARCH_MAP_x86 := 386
+NDK_GO_ARCH_MAP_x86_64 := amd64
+NDK_GO_ARCH_MAP_arm := arm
+NDK_GO_ARCH_MAP_arm64 := arm64
+NDK_GO_ARCH_MAP_mips := mipsx
+NDK_GO_ARCH_MAP_mips64 := mips64x
+
+comma := ,
+CLANG_FLAGS := --target=$(TARGET) --sysroot=$(SYSROOT)
+export CGO_CFLAGS := $(CLANG_FLAGS) $(subst -mthumb,-marm,$(CFLAGS))
+export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(comma)--build-id=none,$(LDFLAGS)) -Wl,-soname=libwg-go.so
+export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
+export GOOS := android
+export CGO_ENABLED := 1
+
+GO_VERSION := 1.21.3
+GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
+GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
+GO_HASH_darwin-amd64 := 27014fc69e301d7588a169ca239b3cc609f0aa1abf38528bf0d20d3b259211eb
+GO_HASH_darwin-arm64 := 65302a7a9f7a4834932b3a7a14cb8be51beddda757b567a2f9e0cbd0d7b5a6ab
+GO_HASH_linux-amd64 := 1241381b2843fae5a9707eec1f8fb2ef94d827990582c7c7c32f5bdfbfd420c8
+
+default: $(DESTDIR)/libwg-go.so
+
+$(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
+ mkdir -p "$(dir $@)"
+ flock "$@.lock" -c ' \
+ [ -f "$@" ] && exit 0; \
+ curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
+ echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
+ mv "$@.tmp" "$@"'
+
+$(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
+ mkdir -p "$(dir $@)"
+ flock "$@.lock" -c ' \
+ [ -f "$@" ] && exit 0; \
+ tar -C "$(dir $@)" --strip-components=1 -xzf "$^" && \
+ patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
+ touch "$@"'
+
+$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
+$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
+ go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared
+
+.DELETE_ON_ERROR:
diff --git a/tunnel/tools/libwg-go/api-android.go b/tunnel/tools/libwg-go/api-android.go
new file mode 100644
index 00000000..d47c5d76
--- /dev/null
+++ b/tunnel/tools/libwg-go/api-android.go
@@ -0,0 +1,227 @@
+/* SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2017-2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+package main
+
+// #cgo LDFLAGS: -llog
+// #include <android/log.h>
+import "C"
+
+import (
+ "fmt"
+ "math"
+ "net"
+ "os"
+ "os/signal"
+ "runtime"
+ "runtime/debug"
+ "strings"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+ "golang.zx2c4.com/wireguard/conn"
+ "golang.zx2c4.com/wireguard/device"
+ "golang.zx2c4.com/wireguard/ipc"
+ "golang.zx2c4.com/wireguard/tun"
+)
+
+type AndroidLogger struct {
+ level C.int
+ tag *C.char
+}
+
+func cstring(s string) *C.char {
+ b, err := unix.BytePtrFromString(s)
+ if err != nil {
+ b := [1]C.char{}
+ return &b[0]
+ }
+ return (*C.char)(unsafe.Pointer(b))
+}
+
+func (l AndroidLogger) Printf(format string, args ...interface{}) {
+ C.__android_log_write(l.level, l.tag, cstring(fmt.Sprintf(format, args...)))
+}
+
+type TunnelHandle struct {
+ device *device.Device
+ uapi net.Listener
+}
+
+var tunnelHandles map[int32]TunnelHandle
+
+func init() {
+ tunnelHandles = make(map[int32]TunnelHandle)
+ signals := make(chan os.Signal)
+ signal.Notify(signals, unix.SIGUSR2)
+ go func() {
+ buf := make([]byte, os.Getpagesize())
+ for {
+ select {
+ case <-signals:
+ n := runtime.Stack(buf, true)
+ if n == len(buf) {
+ n--
+ }
+ buf[n] = 0
+ C.__android_log_write(C.ANDROID_LOG_ERROR, cstring("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
+ }
+ }
+ }()
+}
+
+//export wgTurnOn
+func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 {
+ tag := cstring("WireGuard/GoBackend/" + interfaceName)
+ logger := &device.Logger{
+ Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf,
+ Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf,
+ }
+
+ tun, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
+ if err != nil {
+ unix.Close(int(tunFd))
+ logger.Errorf("CreateUnmonitoredTUNFromFD: %v", err)
+ return -1
+ }
+
+ logger.Verbosef("Attaching to interface %v", name)
+ device := device.NewDevice(tun, conn.NewStdNetBind(), logger)
+
+ err = device.IpcSet(settings)
+ if err != nil {
+ unix.Close(int(tunFd))
+ logger.Errorf("IpcSet: %v", err)
+ return -1
+ }
+ device.DisableSomeRoamingForBrokenMobileSemantics()
+
+ var uapi net.Listener
+
+ uapiFile, err := ipc.UAPIOpen(name)
+ if err != nil {
+ logger.Errorf("UAPIOpen: %v", err)
+ } else {
+ uapi, err = ipc.UAPIListen(name, uapiFile)
+ if err != nil {
+ uapiFile.Close()
+ logger.Errorf("UAPIListen: %v", err)
+ } else {
+ go func() {
+ for {
+ conn, err := uapi.Accept()
+ if err != nil {
+ return
+ }
+ go device.IpcHandle(conn)
+ }
+ }()
+ }
+ }
+
+ err = device.Up()
+ if err != nil {
+ logger.Errorf("Unable to bring up device: %v", err)
+ uapiFile.Close()
+ device.Close()
+ return -1
+ }
+ logger.Verbosef("Device started")
+
+ var i int32
+ for i = 0; i < math.MaxInt32; i++ {
+ if _, exists := tunnelHandles[i]; !exists {
+ break
+ }
+ }
+ if i == math.MaxInt32 {
+ logger.Errorf("Unable to find empty handle")
+ uapiFile.Close()
+ device.Close()
+ return -1
+ }
+ tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
+ return i
+}
+
+//export wgTurnOff
+func wgTurnOff(tunnelHandle int32) {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return
+ }
+ delete(tunnelHandles, tunnelHandle)
+ if handle.uapi != nil {
+ handle.uapi.Close()
+ }
+ handle.device.Close()
+}
+
+//export wgGetSocketV4
+func wgGetSocketV4(tunnelHandle int32) int32 {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return -1
+ }
+ bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
+ if bind == nil {
+ return -1
+ }
+ fd, err := bind.PeekLookAtSocketFd4()
+ if err != nil {
+ return -1
+ }
+ return int32(fd)
+}
+
+//export wgGetSocketV6
+func wgGetSocketV6(tunnelHandle int32) int32 {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return -1
+ }
+ bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
+ if bind == nil {
+ return -1
+ }
+ fd, err := bind.PeekLookAtSocketFd6()
+ if err != nil {
+ return -1
+ }
+ return int32(fd)
+}
+
+//export wgGetConfig
+func wgGetConfig(tunnelHandle int32) *C.char {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return nil
+ }
+ settings, err := handle.device.IpcGet()
+ if err != nil {
+ return nil
+ }
+ return C.CString(settings)
+}
+
+//export wgVersion
+func wgVersion() *C.char {
+ info, ok := debug.ReadBuildInfo()
+ if !ok {
+ return C.CString("unknown")
+ }
+ for _, dep := range info.Deps {
+ if dep.Path == "golang.zx2c4.com/wireguard" {
+ parts := strings.Split(dep.Version, "-")
+ if len(parts) == 3 && len(parts[2]) == 12 {
+ return C.CString(parts[2][:7])
+ }
+ return C.CString(dep.Version)
+ }
+ }
+ return C.CString("unknown")
+}
+
+func main() {}
diff --git a/tunnel/tools/libwg-go/go.mod b/tunnel/tools/libwg-go/go.mod
new file mode 100644
index 00000000..9f9381ff
--- /dev/null
+++ b/tunnel/tools/libwg-go/go.mod
@@ -0,0 +1,14 @@
+module golang.zx2c4.com/wireguard/android
+
+go 1.20
+
+require (
+ golang.org/x/sys v0.13.0
+ golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb
+)
+
+require (
+ golang.org/x/crypto v0.14.0 // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
+)
diff --git a/tunnel/tools/libwg-go/go.sum b/tunnel/tools/libwg-go/go.sum
new file mode 100644
index 00000000..e5f8fc83
--- /dev/null
+++ b/tunnel/tools/libwg-go/go.sum
@@ -0,0 +1,13 @@
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
+golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb h1:c5tyN8sSp8jSDxdCCDXVOpJwYXXhmTkNMt+g0zTSOic=
+golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
+gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
diff --git a/app/tools/libwg-go/goruntime-boottime-over-monotonic.diff b/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff
index 5fd02397..5d78242b 100644
--- a/app/tools/libwg-go/goruntime-boottime-over-monotonic.diff
+++ b/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff
@@ -1,7 +1,8 @@
-From b19623e7673a4d6743745382d5d38751b64e011d Mon Sep 17 00:00:00 2001
+From 61f3ae8298d1c503cbc31539e0f3a73446c7db9d Mon Sep 17 00:00:00 2001
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
-Date: Wed, 27 Feb 2019 05:05:44 +0100
-Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
+Date: Tue, 21 Mar 2023 15:33:56 +0100
+Subject: [PATCH] [release-branch.go1.20] runtime: use CLOCK_BOOTTIME in
+ nanotime on Linux
This makes timers account for having expired while a computer was
asleep, which is quite common on mobile devices. Note that BOOTTIME is
@@ -21,17 +22,17 @@ Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
src/runtime/sys_linux_amd64.s | 2 +-
src/runtime/sys_linux_arm.s | 4 ++--
src/runtime/sys_linux_arm64.s | 4 ++--
- src/runtime/sys_linux_mips64x.s | 2 +-
+ src/runtime/sys_linux_mips64x.s | 4 ++--
src/runtime/sys_linux_mipsx.s | 2 +-
src/runtime/sys_linux_ppc64x.s | 2 +-
src/runtime/sys_linux_s390x.s | 2 +-
- 8 files changed, 11 insertions(+), 11 deletions(-)
+ 8 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
-index 72c43bd9da..daadfe32a9 100644
+index 12a294153d..17e3524b40 100644
--- a/src/runtime/sys_linux_386.s
+++ b/src/runtime/sys_linux_386.s
-@@ -288,13 +288,13 @@ noswitch:
+@@ -352,13 +352,13 @@ noswitch:
LEAL 8(SP), BX // &ts (struct timespec)
MOVL BX, 4(SP)
@@ -48,20 +49,20 @@ index 72c43bd9da..daadfe32a9 100644
INVOKE_SYSCALL
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
-index 5c300f553d..e4a6f12ec6 100644
+index c7a89ba536..01f0a6a26e 100644
--- a/src/runtime/sys_linux_amd64.s
+++ b/src/runtime/sys_linux_amd64.s
-@@ -261,7 +261,7 @@ noswitch:
- MOVQ runtime·vdsoClockgettimeSym(SB), AX
- CMPQ AX, $0
- JEQ fallback
+@@ -255,7 +255,7 @@ noswitch:
+ SUBQ $16, SP // Space for results
+ ANDQ $~15, SP // Align for C code
+
- MOVL $1, DI // CLOCK_MONOTONIC
+ MOVL $7, DI // CLOCK_BOOTTIME
LEAQ 0(SP), SI
- CALL AX
- MOVQ 0(SP), AX // sec
+ MOVQ runtime·vdsoClockgettimeSym(SB), AX
+ CMPQ AX, $0
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
-index 9c7398451c..61b6cd91f6 100644
+index 7b8c4f0e04..9798a1334e 100644
--- a/src/runtime/sys_linux_arm.s
+++ b/src/runtime/sys_linux_arm.s
@@ -11,7 +11,7 @@
@@ -73,20 +74,20 @@ index 9c7398451c..61b6cd91f6 100644
// for EABI, as we don't support OABI
#define SYS_BASE 0x0
-@@ -291,7 +291,7 @@ noswitch:
- SUB $24, R13 // Space for results
- BIC $0x7, R13 // Align for C code
+@@ -374,7 +374,7 @@ finish:
+ // func nanotime1() int64
+ TEXT runtime·nanotime1(SB),NOSPLIT,$12-8
- MOVW $CLOCK_MONOTONIC, R0
+ MOVW $CLOCK_BOOTTIME, R0
- MOVW $8(R13), R1 // timespec
- MOVW runtime·vdsoClockgettimeSym(SB), R11
- CMP $0, R11
+ MOVW $spec-12(SP), R1 // timespec
+
+ MOVW runtime·vdsoClockgettimeSym(SB), R4
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
-index 2835b6ca1c..346ca9cfce 100644
+index 38ff6ac330..6b819c5441 100644
--- a/src/runtime/sys_linux_arm64.s
+++ b/src/runtime/sys_linux_arm64.s
-@@ -13,7 +13,7 @@
+@@ -14,7 +14,7 @@
#define AT_FDCWD -100
#define CLOCK_REALTIME 0
@@ -95,7 +96,7 @@ index 2835b6ca1c..346ca9cfce 100644
#define SYS_exit 93
#define SYS_read 63
-@@ -247,7 +247,7 @@ noswitch:
+@@ -338,7 +338,7 @@ noswitch:
BIC $15, R1
MOVD R1, RSP
@@ -103,59 +104,68 @@ index 2835b6ca1c..346ca9cfce 100644
+ MOVW $CLOCK_BOOTTIME, R0
MOVD runtime·vdsoClockgettimeSym(SB), R2
CBZ R2, fallback
- BL (R2)
+
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
-index 33ed1050c2..59a5be179c 100644
+index 47f2da524d..a8b387f193 100644
--- a/src/runtime/sys_linux_mips64x.s
+++ b/src/runtime/sys_linux_mips64x.s
-@@ -189,7 +189,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$16
- RET
+@@ -326,7 +326,7 @@ noswitch:
+ AND $~15, R1 // Align for C code
+ MOVV R1, R29
- TEXT runtime·nanotime(SB),NOSPLIT,$16
- MOVW $1, R4 // CLOCK_MONOTONIC
+ MOVW $7, R4 // CLOCK_BOOTTIME
MOVV $0(R29), R5
- MOVV $SYS_clock_gettime, R2
- SYSCALL
+
+ MOVV runtime·vdsoClockgettimeSym(SB), R25
+@@ -336,7 +336,7 @@ noswitch:
+ // see walltime for detail
+ BEQ R2, R0, finish
+ MOVV R0, runtime·vdsoClockgettimeSym(SB)
+- MOVW $1, R4 // CLOCK_MONOTONIC
++ MOVW $7, R4 // CLOCK_BOOTTIME
+ MOVV $0(R29), R5
+ JMP fallback
+
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
-index 6e539fbc6f..55b2bf7156 100644
+index 5e6b6c1504..7f5fd2a80e 100644
--- a/src/runtime/sys_linux_mipsx.s
+++ b/src/runtime/sys_linux_mipsx.s
-@@ -194,7 +194,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12
+@@ -243,7 +243,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12
RET
- TEXT runtime·nanotime(SB),NOSPLIT,$8-8
+ TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
- MOVW $1, R4 // CLOCK_MONOTONIC
+ MOVW $7, R4 // CLOCK_BOOTTIME
MOVW $4(R29), R5
MOVW $SYS_clock_gettime, R2
SYSCALL
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
-index 13d23156bd..f67e5062aa 100644
+index d0427a4807..05ee9fede9 100644
--- a/src/runtime/sys_linux_ppc64x.s
+++ b/src/runtime/sys_linux_ppc64x.s
-@@ -204,7 +204,7 @@ fallback:
- JMP finish
+@@ -298,7 +298,7 @@ fallback:
+ JMP return
- TEXT runtime·nanotime(SB),NOSPLIT,$16
+ TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
- MOVD $1, R3 // CLOCK_MONOTONIC
+ MOVD $7, R3 // CLOCK_BOOTTIME
MOVD R1, R15 // R15 is unchanged by C code
MOVD g_m(g), R21 // R21 = m
diff --git a/src/runtime/sys_linux_s390x.s b/src/runtime/sys_linux_s390x.s
-index 58b36dff0a..cb92e9a402 100644
+index 1448670b91..7d2ee3231c 100644
--- a/src/runtime/sys_linux_s390x.s
+++ b/src/runtime/sys_linux_s390x.s
-@@ -180,7 +180,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$16
+@@ -296,7 +296,7 @@ fallback:
RET
- TEXT runtime·nanotime(SB),NOSPLIT,$16
-- MOVW $1, R2 // CLOCK_MONOTONIC
-+ MOVW $7, R2 // CLOCK_BOOTTIME
- MOVD $tp-16(SP), R3
- MOVW $SYS_clock_gettime, R1
- SYSCALL
+ TEXT runtime·nanotime1(SB),NOSPLIT,$32-8
+- MOVW $1, R2 // CLOCK_MONOTONIC
++ MOVW $7, R2 // CLOCK_BOOTTIME
+
+ MOVD R15, R7 // Backup stack pointer
+
--
-2.23.0
+2.17.1
diff --git a/app/tools/libwg-go/jni.c b/tunnel/tools/libwg-go/jni.c
index f6229a49..7ad94d35 100644
--- a/app/tools/libwg-go/jni.c
+++ b/tunnel/tools/libwg-go/jni.c
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: Apache-2.0
*
- * Copyright © 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * Copyright © 2017-2021 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
#include <jni.h>
@@ -12,6 +12,7 @@ extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settin
extern void wgTurnOff(int handle);
extern int wgGetSocketV4(int handle);
extern int wgGetSocketV6(int handle);
+extern char *wgGetConfig(int handle);
extern char *wgVersion();
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
@@ -47,6 +48,17 @@ JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV
return wgGetSocketV6(handle);
}
+JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle)
+{
+ jstring ret;
+ char *config = wgGetConfig(handle);
+ if (!config)
+ return NULL;
+ ret = (*env)->NewStringUTF(env, config);
+ free(config);
+ return ret;
+}
+
JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
{
jstring ret;
diff --git a/tunnel/tools/ndk-compat/compat.c b/tunnel/tools/ndk-compat/compat.c
new file mode 100644
index 00000000..c5ce85ec
--- /dev/null
+++ b/tunnel/tools/ndk-compat/compat.c
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: BSD
+ *
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ *
+ */
+
+#define FILE_IS_EMPTY
+
+#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
+#undef FILE_IS_EMPTY
+#include <string.h>
+
+char *strchrnul(const char *s, int c)
+{
+ char *x = strchr(s, c);
+ if (!x)
+ return (char *)s + strlen(s);
+ return x;
+}
+#endif
+
+#ifdef FILE_IS_EMPTY
+#undef FILE_IS_EMPTY
+static char ____x __attribute__((unused));
+#endif
diff --git a/tunnel/tools/ndk-compat/compat.h b/tunnel/tools/ndk-compat/compat.h
new file mode 100644
index 00000000..7dfd8e14
--- /dev/null
+++ b/tunnel/tools/ndk-compat/compat.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: BSD
+ *
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ *
+ */
+
+#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
+char *strchrnul(const char *s, int c);
+#endif
+
diff --git a/tunnel/tools/wireguard-tools b/tunnel/tools/wireguard-tools
new file mode 160000
+Subproject b4f6b4f229d291daf7c35c6f1e7f4841cc6d69b
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
new file mode 100644
index 00000000..5cdcb6a1
--- /dev/null
+++ b/ui/build.gradle.kts
@@ -0,0 +1,93 @@
+@file:Suppress("UnstableApiUsage")
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+val pkg: String = providers.gradleProperty("wireguardPackageName").get()
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.kapt)
+}
+
+android {
+ compileSdk = 34
+ buildFeatures {
+ buildConfig = true
+ dataBinding = true
+ viewBinding = true
+ }
+ namespace = pkg
+ defaultConfig {
+ applicationId = pkg
+ minSdk = 21
+ targetSdk = 34
+ versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
+ versionName = providers.gradleProperty("wireguardVersionName").get()
+ buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles("proguard-android-optimize.txt")
+ packaging {
+ resources {
+ excludes += "DebugProbesKt.bin"
+ excludes += "kotlin-tooling-metadata.json"
+ excludes += "META-INF/*.version"
+ }
+ }
+ }
+ debug {
+ applicationIdSuffix = ".debug"
+ versionNameSuffix = "-debug"
+ }
+ create("googleplay") {
+ initWith(getByName("release"))
+ matchingFallbacks += "release"
+ }
+ }
+ androidResources {
+ generateLocaleConfig = true
+ }
+ lint {
+ disable += "LongLogTag"
+ warning += "MissingTranslation"
+ warning += "ImpliedQuantity"
+ }
+}
+
+dependencies {
+ implementation(project(":tunnel"))
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.annotation)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.coordinatorlayout)
+ implementation(libs.androidx.biometric)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.fragment.ktx)
+ implementation(libs.androidx.preference.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.datastore.preferences)
+ implementation(libs.google.material)
+ implementation(libs.zxing.android.embedded)
+ implementation(libs.kotlinx.coroutines.android)
+ coreLibraryDesugaring(libs.desugarJdkLibs)
+}
+
+tasks.withType<JavaCompile>().configureEach {
+ options.compilerArgs.add("-Xlint:unchecked")
+ options.isDeprecation = true
+}
+
+tasks.withType<KotlinCompile>().configureEach {
+ compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
+}
diff --git a/ui/proguard-android-optimize.txt b/ui/proguard-android-optimize.txt
new file mode 100644
index 00000000..7bbc2b88
--- /dev/null
+++ b/ui/proguard-android-optimize.txt
@@ -0,0 +1,35 @@
+-allowaccessmodification
+-dontusemixedcaseclassnames
+-dontobfuscate
+-verbose
+
+-keepattributes *Annotation*
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keepclassmembers class * implements android.os.Parcelable {
+ public static final ** CREATOR;
+}
+
+-keep class androidx.annotation.Keep
+
+-keep @androidx.annotation.Keep class * {*;}
+
+-keepclasseswithmembers class * {
+ @androidx.annotation.Keep <methods>;
+}
+
+-keepclasseswithmembers class * {
+ @androidx.annotation.Keep <fields>;
+}
+
+-keepclasseswithmembers class * {
+ @androidx.annotation.Keep <init>(...);
+}
diff --git a/ui/sampledata/interface_names.json b/ui/sampledata/interface_names.json
new file mode 100644
index 00000000..1c41cb22
--- /dev/null
+++ b/ui/sampledata/interface_names.json
@@ -0,0 +1,34 @@
+{
+ "comment": "Interface names",
+ "names": [
+ {
+ "names": [
+ { "name": "wg0" },
+ { "name": "wg1" },
+ { "name": "wg2" },
+ { "name": "wg3" },
+ { "name": "wg4" },
+ { "name": "wg5" },
+ { "name": "wg6" },
+ { "name": "wg7" },
+ { "name": "wg8" },
+ { "name": "wg9" },
+ { "name": "wg10" },
+ { "name": "wg11" }
+ ],
+ "checked": [
+ { "checked": true },
+ { "checked": false },
+ { "checked": true },
+ { "checked": false },
+ { "checked": true },
+ { "checked": false },
+ { "checked": true },
+ { "checked": false },
+ { "checked": true },
+ { "checked": false },
+ { "checked": true }
+ ]
+ }
+ ]
+}
diff --git a/ui/src/debug/res/values/strings.xml b/ui/src/debug/res/values/strings.xml
new file mode 100644
index 00000000..947b7381
--- /dev/null
+++ b/ui/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name" translatable="false">WireGuard β</string>
+</resources>
diff --git a/ui/src/googleplay/AndroidManifest.xml b/ui/src/googleplay/AndroidManifest.xml
new file mode 100644
index 00000000..6d64f732
--- /dev/null
+++ b/ui/src/googleplay/AndroidManifest.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission
+ android:name="android.permission.REQUEST_INSTALL_PACKAGES"
+ tools:node="remove" />
+</manifest>
diff --git a/app/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index 67653221..754992d5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -1,13 +1,32 @@
<?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.WRITE_EXTERNAL_STORAGE" />
+ <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"
+ tools:ignore="ScopedStorage" />
+
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+ <uses-feature
+ android:name="android.software.leanback"
+ android:required="false" />
+ <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"
@@ -19,15 +38,22 @@
<application
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"
android:supportsRtl="true"
- android:theme="@style/AppTheme"
- tools:ignore="UnusedAttribute">
+ android:theme="@style/AppTheme">
- <activity android:name=".activity.MainActivity">
+ <activity
+ android:name=".activity.TunnelToggleActivity"
+ android:excludeFromRecents="true"
+ android:theme="@style/NoBackgroundTheme" />
+ <activity
+ android:name=".activity.MainActivity"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -40,6 +66,16 @@
</activity>
<activity
+ android:name=".activity.TvMainActivity"
+ android:exported="true"
+ android:theme="@style/TvTheme">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
android:name=".activity.SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".activity.MainActivity" />
@@ -54,7 +90,24 @@
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
- <receiver android:name=".BootShutdownReceiver">
+ <activity
+ android:name=".activity.LogViewerActivity"
+ android:exported="false"
+ android:label="@string/log_viewer_title">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <provider
+ android:name=".activity.LogViewerActivity$ExportedLogContentProvider"
+ android:authorities="${applicationId}.exported-log"
+ android:exported="false"
+ android:grantUriPermissions="true" />
+
+ <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" />
@@ -62,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" />
@@ -72,15 +134,8 @@
</receiver>
<service
- android:name=".backend.GoBackend$VpnService"
- android:permission="android.permission.BIND_VPN_SERVICE">
- <intent-filter>
- <action android:name="android.net.VpnService" />
- </intent-filter>
- </service>
-
- <service
android:name=".QuickTileService"
+ android:exported="true"
android:icon="@drawable/ic_tile"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
@@ -91,6 +146,21 @@
<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
+ android:name="android.content.APP_RESTRICTIONS"
+ android:resource="@xml/app_restrictions" />
</application>
+
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent>
+ </queries>
</manifest>
diff --git a/ui/src/main/java/com/wireguard/android/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt
new file mode 100644
index 00000000..4dcec508
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/Application.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy
+import android.os.StrictMode.VmPolicy
+import android.util.Log
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.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
+import com.wireguard.android.util.applicationScope
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import java.lang.ref.WeakReference
+import java.util.Locale
+
+class Application : android.app.Application() {
+ private val futureBackend = CompletableDeferred<Backend>()
+ private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main.immediate)
+ private var backend: Backend? = null
+ private lateinit var rootShell: RootShell
+ private lateinit var preferencesDataStore: DataStore<Preferences>
+ private lateinit var toolsInstaller: ToolsInstaller
+ private lateinit var tunnelManager: TunnelManager
+
+ override fun attachBaseContext(context: Context) {
+ super.attachBaseContext(context)
+ if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
+ 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)
+ startActivity(intent)
+ System.exit(0)
+ }
+ }
+
+ private suspend fun determineBackend(): Backend {
+ var backend: Backend? = null
+ if (UserKnobs.enableKernelModule.first() && WgQuickBackend.hasKernelSupport()) {
+ try {
+ rootShell.start()
+ val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
+ wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
+ backend = wgQuickBackend
+ UserKnobs.multipleTunnels.onEach {
+ wgQuickBackend.setMultipleTunnels(it)
+ }.launchIn(coroutineScope)
+ } catch (ignored: Exception) {
+ }
+ }
+ if (backend == null) {
+ backend = GoBackend(applicationContext)
+ GoBackend.setAlwaysOnCallback { get().applicationScope.launch { get().tunnelManager.restoreState(true) } }
+ }
+ return backend
+ }
+
+ override fun onCreate() {
+ Log.i(TAG, USER_AGENT)
+ super.onCreate()
+ 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) {
+ 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)
+ }
+ tunnelManager = TunnelManager(FileConfigStore(applicationContext))
+ tunnelManager.onCreate()
+ coroutineScope.launch(Dispatchers.IO) {
+ try {
+ backend = determineBackend()
+ futureBackend.complete(backend!!)
+ } catch (e: Throwable) {
+ 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() {
+ coroutineScope.cancel()
+ super.onTerminate()
+ }
+
+ companion object {
+ val USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, if (Build.SUPPORTED_ABIS.isNotEmpty()) Build.SUPPORTED_ABIS[0] else "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT)
+ private const val TAG = "WireGuard/Application"
+ private lateinit var weakSelf: WeakReference<Application>
+
+ fun get(): Application {
+ return weakSelf.get()!!
+ }
+
+ suspend fun getBackend() = get().futureBackend.await()
+
+ fun getRootShell() = get().rootShell
+
+ fun getPreferencesDataStore() = get().preferencesDataStore
+
+ fun getToolsInstaller() = get().toolsInstaller
+
+ fun getTunnelManager() = get().tunnelManager
+
+ fun getCoroutineScope() = get().coroutineScope
+ }
+
+ init {
+ weakSelf = WeakReference(this)
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
new file mode 100644
index 00000000..a859bf71
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.wireguard.android.backend.WgQuickBackend
+import com.wireguard.android.util.applicationScope
+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 tunnelManager = Application.getTunnelManager()
+ if (Intent.ACTION_BOOT_COMPLETED == action) {
+ Log.i(TAG, "Broadcast receiver restoring state (boot)")
+ tunnelManager.restoreState(false)
+ } else if (Intent.ACTION_SHUTDOWN == action) {
+ Log.i(TAG, "Broadcast receiver saving state (shutdown)")
+ tunnelManager.saveState()
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/BootShutdownReceiver"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
new file mode 100644
index 00000000..9be2cc2b
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
@@ -0,0 +1,187 @@
+/*
+ * 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
+import androidx.annotation.RequiresApi
+import androidx.databinding.Observable
+import androidx.databinding.Observable.OnPropertyChangedCallback
+import com.wireguard.android.activity.MainActivity
+import com.wireguard.android.activity.TunnelToggleActivity
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.applicationScope
+import com.wireguard.android.widget.SlashDrawable
+import kotlinx.coroutines.launch
+
+/**
+ * Service that maintains the application's custom Quick Settings tile. This service is bound by the
+ * system framework as necessary to update the appearance of the tile in the system UI, and to
+ * forward click events to the application.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+class QuickTileService : TileService() {
+ private val onStateChangedCallback = OnStateChangedCallback()
+ private val onTunnelChangedCallback = OnTunnelChangedCallback()
+ private var iconOff: Icon? = null
+ private var iconOn: Icon? = null
+ private var tunnel: ObservableTunnel? = null
+
+ /* This works around an annoying unsolved frameworks bug some people are hitting. */
+ override fun onBind(intent: Intent): IBinder? {
+ var ret: IBinder? = null
+ try {
+ ret = super.onBind(intent)
+ } catch (e: Throwable) {
+ Log.d(TAG, "Failed to bind to TileService", e)
+ }
+ return ret
+ }
+
+ override fun onClick() {
+ 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)
+ }
+ }
+ 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)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreate() {
+ isAdded = true
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
+ iconOff = iconOn
+ return
+ }
+ val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
+ icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
+ icon.setSlashed(false)
+ var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
+ 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)
+ 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)
+ tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
+ updateTile()
+ }
+
+ override fun onStopListening() {
+ 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) {
+ tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
+ tunnel = newTunnel
+ tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
+ }
+ // Update the tile contents.
+ 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()
+ }
+
+ private inner class OnStateChangedCallback : OnPropertyChangedCallback() {
+ override fun onPropertyChanged(sender: Observable, propertyId: Int) {
+ if (sender != tunnel) {
+ sender.removeOnPropertyChangedCallback(this)
+ 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
+ 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
new file mode 100644
index 00000000..56810377
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt
@@ -0,0 +1,96 @@
+/*
+ * 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 : AppCompatActivity() {
+ private val selectionChangeRegistry = SelectionChangeRegistry()
+ private var created = false
+ var selectedTunnel: ObservableTunnel? = null
+ set(value) {
+ val oldTunnel = field
+ if (oldTunnel == value) return
+ field = value
+ if (created) {
+ if (!onSelectedTunnelChanged(oldTunnel, value)) {
+ field = oldTunnel
+ } else {
+ selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
+ }
+ }
+ }
+
+ fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
+ selectionChangeRegistry.add(listener)
+ }
+
+ 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.launch {
+ val tunnel = Application.getTunnelManager().getTunnels()[savedTunnelName]
+ if (tunnel == null)
+ created = true
+ selectedTunnel = tunnel
+ created = true
+ }
+ } else {
+ created = true
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ if (selectedTunnel != null) outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel!!.name)
+ super.onSaveInstanceState(outState)
+ }
+
+ protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
+
+ fun removeOnSelectedTunnelChangedListener(
+ listener: OnSelectedTunnelChangedListener
+ ) {
+ selectionChangeRegistry.remove(listener)
+ }
+
+ interface OnSelectedTunnelChangedListener {
+ fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
+ }
+
+ private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() {
+ override fun onNotifyCallback(
+ listener: OnSelectedTunnelChangedListener,
+ oldTunnel: ObservableTunnel?,
+ ignored: Int,
+ newTunnel: ObservableTunnel?
+ ) {
+ listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
+ }
+ }
+
+ private class SelectionChangeRegistry :
+ 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
new file mode 100644
index 00000000..69e33bc9
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.activity
+
+import android.content.ClipDescription.compareMimeTypes
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Intent
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.graphics.Typeface.BOLD
+import android.net.Uri
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.ForegroundColorSpan
+import android.text.style.StyleSpan
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.collection.CircularArray
+import androidx.core.app.ShareCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.textview.MaterialTextView
+import com.wireguard.android.BuildConfig
+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
+import kotlinx.coroutines.withContext
+import java.io.BufferedReader
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+import java.text.DateFormat
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+class LogViewerActivity : AppCompatActivity() {
+ private lateinit var binding: LogViewerActivityBinding
+ private lateinit var logAdapter: LogEntryAdapter
+ private var logLines = CircularArray<LogLine>()
+ private var rawLogLines = CircularArray<String>()
+ private var recyclerView: RecyclerView? = null
+ private var saveButton: MenuItem? = null
+ private val year by lazy {
+ val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
+ yearFormatter.format(Date())
+ }
+
+ 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) }
+
+ private val errorColor by lazy { ResourcesCompat.getColor(resources, R.color.error_tag_color, theme) }
+
+ private val infoColor by lazy { ResourcesCompat.getColor(resources, R.color.info_tag_color, theme) }
+
+ private val warningColor by lazy { ResourcesCompat.getColor(resources, R.color.warning_tag_color, theme) }
+
+ private var lastUri: Uri? = null
+
+ private fun revokeLastUri() {
+ lastUri?.let {
+ LOGS.remove(it.pathSegments.lastOrNull())
+ revokeUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ lastUri = null
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = LogViewerActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ logAdapter = LogEntryAdapter()
+ binding.recyclerView.apply {
+ recyclerView = this
+ layoutManager = LinearLayoutManager(context)
+ adapter = logAdapter
+ addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
+ }
+
+ lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
+
+ val revokeLastActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ revokeLastUri()
+ }
+
+ binding.shareFab.setOnClickListener {
+ 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)
+ }
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.log_viewer, menu)
+ saveButton = menu.findItem(R.id.save_log)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ 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(rawLogBytes())
+ } catch (e: Throwable) {
+ outputFile?.delete()
+ exception = e
+ }
+ }
+ saveButton?.isEnabled = true
+ if (outputFile == null)
+ return
+ Snackbar.make(
+ findViewById(android.R.id.content),
+ if (exception == null) getString(R.string.log_export_success, outputFile?.fileName)
+ else getString(R.string.log_export_error, ErrorMessages[exception]),
+ if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
+ )
+ .setAnchorView(binding.shareFab)
+ .show()
+ }
+
+ private suspend fun streamingLog() = withContext(Dispatchers.IO) {
+ val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
+ builder.environment()["LC_ALL"] = "C"
+ var process: Process? = null
+ try {
+ process = try {
+ builder.start()
+ } catch (e: IOException) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ return@withContext
+ }
+ val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
+
+ var 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
+ 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) {
+ 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)
+ }
+ bufferedLogLines.clear()
+ logAdapter.notifyItemRangeInserted(posStart, logLines.size() - posStart)
+ posStart = logLines.size()
+
+ if (isScrolledToBottomAlready) {
+ recyclerView?.scrollToPosition(logLines.size() - 1)
+ }
+ }
+ }
+ } finally {
+ process?.destroy()
+ }
+ }
+
+ private fun parseTime(timeStr: String): Date? {
+ val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ return try {
+ formatter.parse("$year-$timeStr")
+ } catch (e: ParseException) {
+ null
+ }
+ }
+
+ private fun parseLine(line: String): LogLine? {
+ val m: Matcher = THREADTIME_LINE.matcher(line)
+ return if (m.matches()) {
+ LogLine(m.group(2)!!.toInt(), m.group(3)!!.toInt(), parseTime(m.group(1)!!), m.group(4)!!, m.group(5)!!, m.group(6)!!)
+ } else {
+ null
+ }
+ }
+
+ private data class LogLine(val pid: Int, val tid: Int, val time: Date?, val level: String, val tag: String, var msg: String)
+
+ companion object {
+ /**
+ * Match a single line of `logcat -v threadtime`, such as:
+ *
+ * <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 LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
+ private const val TAG = "WireGuard/LogViewerActivity"
+ }
+
+ private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
+
+ private inner class ViewHolder(val layout: View, var isSingleLine: Boolean = true) : RecyclerView.ViewHolder(layout)
+
+ private fun levelToColor(level: String): Int {
+ return when (level) {
+ "V", "D" -> debugColor
+ "E" -> errorColor
+ "I" -> infoColor
+ "W" -> warningColor
+ else -> defaultColor
+ }
+ }
+
+ 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)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val line = logLines[position]
+ val spannable = if (position > 0 && logLines[position - 1].tag == line.tag)
+ SpannableString(line.msg)
+ 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
+ )
+ }
+ holder.layout.apply {
+ findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString()
+ findViewById<MaterialTextView>(R.id.log_msg).apply {
+ setSingleLine()
+ text = spannable
+ setOnClickListener {
+ isSingleLine = !holder.isSingleLine
+ holder.isSingleLine = !holder.isSingleLine
+ }
+ }
+ }
+ }
+ }
+
+ class ExportedLogContentProvider : ContentProvider() {
+ private fun logForUri(uri: Uri): ByteArray? = LOGS[uri.pathSegments.lastOrNull()]
+
+ 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
+ }
+
+ override fun onCreate(): Boolean = true
+
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
+
+ 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 openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
+ if (mode != "r") return null
+ val log = logForUri(uri) ?: return null
+ return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
+ try {
+ FileOutputStream(output.fileDescriptor).write(l!!)
+ } catch (_: Throwable) {
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
new file mode 100644
index 00000000..80c4868c
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.activity
+
+import android.content.Intent
+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
+import androidx.fragment.app.commit
+import com.wireguard.android.R
+import com.wireguard.android.fragment.TunnelDetailFragment
+import com.wireguard.android.fragment.TunnelEditorFragment
+import com.wireguard.android.model.ObservableTunnel
+
+/**
+ * CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
+ * WireGuard application, and contains several fragments for listing, viewing details of, and
+ * editing the configuration and interface state of WireGuard tunnels.
+ */
+class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener {
+ private var actionBar: ActionBar? = null
+ private var isTwoPaneLayout = false
+ private var backPressedCallback: OnBackPressedCallback? = null
+
+ 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
+ }
+
+ 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
+ }
+
+ 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 minBackStackEntries = if (isTwoPaneLayout) 2 else 1
+ actionBar!!.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_activity)
+ actionBar = supportActionBar
+ isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null
+ supportFragmentManager.addOnBackStackChangedListener(this)
+ backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() }
+ onBackStackChanged()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.main_activity, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ // The back arrow in the action bar should act the same as the back button.
+ onBackPressedDispatcher.onBackPressed()
+ true
+ }
+
+ R.id.menu_action_edit -> {
+ supportFragmentManager.commit {
+ replace(R.id.detail_container, TunnelEditorFragment())
+ setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ addToBackStack(null)
+ }
+ true
+ }
+ // This menu item is handled by the editor fragment.
+ R.id.menu_action_save -> false
+ R.id.menu_settings -> {
+ startActivity(Intent(this, SettingsActivity::class.java))
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ 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 true
+ }
+ if (backStackEntries == 2) {
+ // Pop the editor off the back stack to reveal the detail fragment. Use the immediate
+ // method to avoid the editor picking up the new tunnel while it is still visible.
+ fragmentManager.popBackStackImmediate()
+ } else if (backStackEntries == 0) {
+ // Create and show a new detail fragment.
+ fragmentManager.commit {
+ add(R.id.detail_container, TunnelDetailFragment())
+ setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+ 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
new file mode 100644
index 00000000..bd6e1f78
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/SettingsActivity.kt
@@ -0,0 +1,107 @@
+/*
+ * 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
+import com.wireguard.android.util.AdminKnobs
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Interface for changing application-global persistent settings.
+ */
+class SettingsActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
+ supportFragmentManager.commit {
+ add(android.R.id.content, SettingsFragment())
+ }
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ finish()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ class SettingsFragment : PreferenceFragmentCompat() {
+ override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
+ preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
+ addPreferencesFromResource(R.xml.preferences)
+ 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)
+ --preferenceScreen.initialExpandedChildrenCount
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ val remoteApps = preferenceManager.findPreference<Preference>("allow_remote_control_intents")
+ remoteApps?.parent?.removePreference(remoteApps)
+ }
+ if (AdminKnobs.disableConfigExport) {
+ val zipExporter = preferenceManager.findPreference<Preference>("zip_exporter")
+ zipExporter?.parent?.removePreference(zipExporter)
+ }
+ val wgQuickOnlyPrefs = arrayOf(
+ preferenceManager.findPreference("tools_installer"),
+ preferenceManager.findPreference("restore_on_boot"),
+ preferenceManager.findPreference<Preference>("multiple_tunnels")
+ ).filterNotNull()
+ wgQuickOnlyPrefs.forEach { it.isVisible = false }
+ lifecycleScope.launch {
+ if (Application.getBackend() is WgQuickBackend) {
+ ++preferenceScreen.initialExpandedChildrenCount
+ wgQuickOnlyPrefs.forEach { it.isVisible = true }
+ } else {
+ wgQuickOnlyPrefs.forEach { it.parent?.removePreference(it) }
+ }
+ }
+ preferenceManager.findPreference<Preference>("log_viewer")?.setOnPreferenceClickListener {
+ startActivity(Intent(requireContext(), LogViewerActivity::class.java))
+ true
+ }
+ val kernelModuleEnabler = preferenceManager.findPreference<Preference>("kernel_module_enabler")
+ if (WgQuickBackend.hasKernelSupport()) {
+ lifecycleScope.launch {
+ if (Application.getBackend() !is WgQuickBackend) {
+ try {
+ withContext(Dispatchers.IO) { Application.getRootShell().start() }
+ } catch (_: Throwable) {
+ kernelModuleEnabler?.parent?.removePreference(kernelModuleEnabler)
+ }
+ }
+ }
+ } else {
+ kernelModuleEnabler?.parent?.removePreference(kernelModuleEnabler)
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt
new file mode 100644
index 00000000..bdf798ca
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.fragment.app.commit
+import com.wireguard.android.fragment.TunnelEditorFragment
+import com.wireguard.android.model.ObservableTunnel
+
+/**
+ * Standalone activity for creating tunnels.
+ */
+class TunnelCreatorActivity : BaseActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
+ supportFragmentManager.commit {
+ add(android.R.id.content, TunnelEditorFragment())
+ }
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..59b9349f
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.os.Build
+import android.os.Bundle
+import android.service.quicksettings.TileService
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.wireguard.android.Application
+import com.wireguard.android.QuickTileService
+import com.wireguard.android.R
+import com.wireguard.android.backend.GoBackend
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.util.ErrorMessages
+import kotlinx.coroutines.launch
+
+@RequiresApi(Build.VERSION_CODES.N)
+class TunnelToggleActivity : AppCompatActivity() {
+ private val permissionActivityResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
+
+ private fun toggleTunnelWithPermissionsResult() {
+ val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
+ lifecycleScope.launch {
+ try {
+ tunnel.setStateAsync(Tunnel.State.TOGGLE)
+ } catch (e: Throwable) {
+ TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
+ val error = ErrorMessages[e]
+ val message = getString(R.string.toggle_error, error)
+ Log.e(TAG, message, e)
+ Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
+ finishAffinity()
+ return@launch
+ }
+ TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
+ finishAffinity()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launch {
+ if (Application.getBackend() is GoBackend) {
+ val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
+ if (intent != null) {
+ permissionActivityResultLauncher.launch(intent)
+ return@launch
+ }
+ }
+ toggleTunnelWithPermissionsResult()
+ }
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/TunnelToggleActivity"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
new file mode 100644
index 00000000..4c86b4c8
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/activity/TvMainActivity.kt
@@ -0,0 +1,445 @@
+/*
+ * 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
+import android.os.Bundle
+import android.os.Environment
+import android.os.storage.StorageManager
+import android.os.storage.StorageVolume
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.activity.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
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.Keyed
+import com.wireguard.android.databinding.ObservableKeyedArrayList
+import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
+import com.wireguard.android.databinding.TvActivityBinding
+import com.wireguard.android.databinding.TvFileListItemBinding
+import com.wireguard.android.databinding.TvTunnelListItemBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.QuantityFormatter
+import com.wireguard.android.util.TunnelImporter
+import com.wireguard.android.util.UserKnobs
+import com.wireguard.android.util.applicationScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+
+class TvMainActivity : AppCompatActivity() {
+ private val tunnelFileImportResultLauncher = registerForActivityResult(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) {
+ Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ private var pendingTunnel: ObservableTunnel? = null
+ private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val tunnel = pendingTunnel
+ if (tunnel != null)
+ setTunnelStateWithPermissionsResult(tunnel)
+ pendingTunnel = null
+ }
+
+ private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel) {
+ lifecycleScope.launch {
+ try {
+ tunnel.setStateAsync(Tunnel.State.TOGGLE)
+ } catch (e: Throwable) {
+ val error = ErrorMessages[e]
+ val message = getString(R.string.error_up, error)
+ Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
+ Log.e(TAG, message, e)
+ }
+ updateStats()
+ }
+ }
+
+ private lateinit var binding: TvActivityBinding
+ private val isDeleting = ObservableBoolean()
+ private val files = ObservableKeyedArrayList<String, KeyedFile>()
+ private val filesRoot = ObservableField("")
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ 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 {
+ binding.tunnels = Application.getTunnelManager().getTunnels()
+ if (binding.tunnels?.isEmpty() == true)
+ binding.importButton.requestFocus()
+ else
+ binding.tunnelList.requestFocus()
+ }
+ binding.isDeleting = isDeleting
+ binding.files = files
+ binding.filesRoot = filesRoot
+ val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
+ gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
+ binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
+ override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
+ binding.isDeleting = isDeleting
+ binding.isFocused = ObservableBoolean()
+ binding.root.setOnFocusChangeListener { _, focused ->
+ binding.isFocused?.set(focused)
+ }
+ binding.root.setOnClickListener {
+ lifecycleScope.launch {
+ if (isDeleting.get()) {
+ try {
+ item.deleteAsync()
+ if (this@TvMainActivity.binding.tunnels?.isEmpty() != false)
+ isDeleting.set(false)
+ } catch (e: Throwable) {
+ val error = ErrorMessages[e]
+ val message = getString(R.string.config_delete_error, error)
+ Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
+ Log.e(TAG, message, e)
+ }
+ } else {
+ if (Application.getBackend() is GoBackend) {
+ val intent = GoBackend.VpnService.prepare(binding.root.context)
+ if (intent != null) {
+ pendingTunnel = item
+ permissionActivityResultLauncher.launch(intent)
+ return@launch
+ }
+ }
+ setTunnelStateWithPermissionsResult(item)
+ }
+ }
+ }
+ }
+ }
+
+ binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
+ override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
+ binding.root.setOnClickListener {
+ if (item.file.isDirectory)
+ navigateTo(item.file)
+ else {
+ val uri = Uri.fromFile(item.file)
+ files.clear()
+ filesRoot.set("")
+ lifecycleScope.launch {
+ TunnelImporter.importTunnel(contentResolver, uri) {
+ Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
+ }
+ }
+ runOnUiThread {
+ this@TvMainActivity.binding.tunnelList.requestFocus()
+ }
+ }
+ }
+ }
+ }
+
+ binding.importButton.setOnClickListener {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if (filesRoot.get()?.isEmpty() != false) {
+ navigateTo(File("/"))
+ runOnUiThread {
+ binding.filesList.requestFocus()
+ }
+ } else {
+ files.clear()
+ filesRoot.set("")
+ runOnUiThread {
+ binding.tunnelList.requestFocus()
+ }
+ }
+ } else {
+ try {
+ tunnelFileImportResultLauncher.launch("*/*")
+ } catch (_: Throwable) {
+ 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()
+ }
+ }
+ }
+
+ binding.deleteButton.setOnClickListener {
+ isDeleting.set(!isDeleting.get())
+ runOnUiThread {
+ 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)
+
+ lifecycleScope.launch {
+ while (true) {
+ updateStats()
+ delay(1000)
+ }
+ }
+ }
+
+ private var pendingNavigation: File? = null
+ private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ val to = pendingNavigation
+ if (it && to != null)
+ navigateTo(to)
+ pendingNavigation = null
+ }
+
+ private var cachedRoots: Collection<KeyedFile>? = null
+
+ private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
+ cachedRoots?.let { return@withContext it }
+ val list = HashSet<KeyedFile>()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val storageManager: StorageManager = getSystemService() ?: return@withContext list
+ list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
+ } else {
+ KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
+ }
+ })
+ } else {
+ @Suppress("DEPRECATION")
+ list.add(KeyedFile(Environment.getExternalStorageDirectory()))
+ try {
+ File("/storage").listFiles()?.forEach {
+ if (!it.isDirectory) return@forEach
+ try {
+ if (Environment.isExternalStorageRemovable(it)) {
+ list.add(KeyedFile(it))
+ }
+ } catch (_: Throwable) {
+ }
+ }
+ } catch (_: Throwable) {
+ }
+ }
+ cachedRoots = list
+ list
+ }
+
+ private fun isBelowCachedRoots(maybeChild: File): Boolean {
+ val cachedRoots = cachedRoots ?: return true
+ for (root in cachedRoots) {
+ if (maybeChild.canonicalPath.startsWith(root.file.canonicalPath))
+ return false
+ }
+ return true
+ }
+
+ private fun navigateTo(directory: File) {
+ require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ pendingNavigation = directory
+ permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
+ return
+ }
+
+ lifecycleScope.launch {
+ if (isBelowCachedRoots(directory)) {
+ val roots = makeStorageRoots()
+ if (roots.count() == 1) {
+ navigateTo(roots.first().file)
+ return@launch
+ }
+ files.clear()
+ files.addAll(roots)
+ filesRoot.set(getString(R.string.tv_select_a_storage_drive))
+ return@launch
+ }
+
+ val newFiles = withContext(Dispatchers.IO) {
+ val newFiles = ArrayList<KeyedFile>()
+ try {
+ directory.parentFile?.let {
+ newFiles.add(KeyedFile(it, "../"))
+ }
+ val listing = directory.listFiles() ?: return@withContext null
+ listing.forEach {
+ if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
+ newFiles.add(KeyedFile(it))
+ }
+ newFiles.sortWith { a, b ->
+ if (a.file.isDirectory && !b.file.isDirectory) -1
+ else if (!a.file.isDirectory && b.file.isDirectory) 1
+ else a.file.compareTo(b.file)
+ }
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ newFiles
+ }
+ if (newFiles?.isEmpty() != false)
+ return@launch
+ files.clear()
+ files.addAll(newFiles)
+ filesRoot.set(directory.canonicalPath)
+ }
+ }
+
+ private fun handleBackPressed() {
+ when {
+ isDeleting.get() -> {
+ isDeleting.set(false)
+ runOnUiThread {
+ binding.tunnelList.requestFocus()
+ }
+ }
+
+ filesRoot.get()?.isNotEmpty() == true -> {
+ files.clear()
+ filesRoot.set("")
+ runOnUiThread {
+ binding.tunnelList.requestFocus()
+ }
+ }
+ }
+ }
+
+ private suspend fun updateStats() {
+ binding.tunnelList.forEach { viewItem ->
+ val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
+ ?: return@forEach
+ try {
+ val tunnel = listItem.item!!
+ if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
+ throw Exception()
+ }
+ val statistics = tunnel.getStatisticsAsync()
+ val rx = statistics.totalRx()
+ val tx = statistics.totalTx()
+ listItem.tunnelTransfer.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
+ listItem.tunnelTransfer.visibility = View.VISIBLE
+ } catch (_: Throwable) {
+ listItem.tunnelTransfer.visibility = View.GONE
+ listItem.tunnelTransfer.text = ""
+ }
+ }
+ }
+
+ class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
+ override val key: String
+ get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
+ }
+
+ private class SlatedSpanSizeLookup(private val gridManager: GridLayoutManager) : SpanSizeLookup() {
+ private val originalHeight = gridManager.spanCount
+ private var newWidth = 0
+ private lateinit var sizeMap: Array<IntArray?>
+
+ private fun emptyUnderIndex(index: Int, size: Int): Int {
+ sizeMap[size - 1]?.let { return it[index] }
+ val sizes = IntArray(size)
+ val oh = originalHeight
+ val nw = newWidth
+ var empties = 0
+ for (i in 0 until size) {
+ val ox = (i + empties) / oh
+ val oy = (i + empties) % oh
+ var empty = 0
+ for (j in oy + 1 until oh) {
+ val ni = nw * j + ox
+ if (ni < size)
+ break
+ empty++
+ }
+ empties += empty
+ sizes[i] = empty
+ }
+ sizeMap[size - 1] = sizes
+ return sizes[index]
+ }
+
+ override fun getSpanSize(position: Int): Int {
+ if (newWidth == 0) {
+ val child = gridManager.getChildAt(0) ?: return 1
+ if (child.width == 0) return 1
+ newWidth = gridManager.width / child.width
+ sizeMap = Array(originalHeight * newWidth - 1) { null }
+ }
+ val total = gridManager.itemCount
+ if (total >= originalHeight * newWidth || total == 0)
+ return 1
+ return emptyUnderIndex(position, total) + 1
+ }
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/TvMainActivity"
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java b/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt
index d4761464..7336e78f 100644
--- a/app/src/main/java/com/wireguard/android/configStore/ConfigStore.java
+++ b/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt
@@ -1,19 +1,15 @@
/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
+package com.wireguard.android.configStore
-package com.wireguard.android.configStore;
-
-import com.wireguard.config.Config;
-
-import java.util.Set;
+import com.wireguard.config.Config
/**
* Interface for persistent storage providers for WireGuard configurations.
*/
-
-public interface ConfigStore {
+interface ConfigStore {
/**
* Create a persistent tunnel, which must have a unique name within the persistent storage
* medium.
@@ -22,46 +18,51 @@ public interface ConfigStore {
* @param config Configuration for the new tunnel.
* @return The configuration that was actually saved to persistent storage.
*/
- Config create(final String name, final Config config) throws Exception;
+ @Throws(Exception::class)
+ fun create(name: String, config: Config): Config
/**
* Delete a persistent tunnel.
*
* @param name The name of the tunnel to delete.
*/
- void delete(final String name) throws Exception;
+ @Throws(Exception::class)
+ fun delete(name: String)
/**
* Enumerate the names of tunnels present in persistent storage.
*
* @return The set of present tunnel names.
*/
- Set<String> enumerate();
+ fun enumerate(): Set<String>
/**
- * Load the configuration for the tunnel given by {@code name}.
+ * Load the configuration for the tunnel given by `name`.
*
* @param name The identifier for the configuration in persistent storage (i.e. the name of the
- * tunnel).
+ * tunnel).
* @return An in-memory representation of the configuration loaded from persistent storage.
*/
- Config load(final String name) throws Exception;
+ @Throws(Exception::class)
+ fun load(name: String): Config
/**
- * Rename the configuration for the tunnel given by {@code name}.
+ * Rename the configuration for the tunnel given by `name`.
*
* @param name The identifier for the existing configuration in persistent storage.
* @param replacement The new identifier for the configuration in persistent storage.
*/
- void rename(String name, String replacement) throws Exception;
+ @Throws(Exception::class)
+ fun rename(name: String, replacement: String)
/**
- * Save the configuration for an existing tunnel given by {@code name}.
+ * Save the configuration for an existing tunnel given by `name`.
*
* @param name The identifier for the configuration in persistent storage (i.e. the name of
- * the tunnel).
+ * the tunnel).
* @param config An updated configuration object for the tunnel.
* @return The configuration that was actually saved to persistent storage.
*/
- Config save(final String name, final Config config) throws Exception;
+ @Throws(Exception::class)
+ fun save(name: String, config: Config): Config
}
diff --git a/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
new file mode 100644
index 00000000..17e3221b
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.configStore
+
+import android.content.Context
+import android.util.Log
+import com.wireguard.android.R
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Config
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+
+/**
+ * Configuration store that uses a `wg-quick`-style file for each configured tunnel.
+ */
+class FileConfigStore(private val context: Context) : ConfigStore {
+ @Throws(IOException::class)
+ override fun create(name: String, config: Config): Config {
+ Log.d(TAG, "Creating configuration for tunnel $name")
+ val file = fileFor(name)
+ if (!file.createNewFile())
+ throw IOException(context.getString(R.string.config_file_exists_error, file.name))
+ FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
+ return config
+ }
+
+ @Throws(IOException::class)
+ override fun delete(name: String) {
+ Log.d(TAG, "Deleting configuration for tunnel $name")
+ val file = fileFor(name)
+ if (!file.delete())
+ throw IOException(context.getString(R.string.config_delete_error, file.name))
+ }
+
+ override fun enumerate(): Set<String> {
+ return context.fileList()
+ .filter { it.endsWith(".conf") }
+ .map { it.substring(0, it.length - ".conf".length) }
+ .toSet()
+ }
+
+ private fun fileFor(name: String): File {
+ return File(context.filesDir, "$name.conf")
+ }
+
+ @Throws(BadConfigException::class, IOException::class)
+ override fun load(name: String): Config {
+ FileInputStream(fileFor(name)).use { stream -> return Config.parse(stream) }
+ }
+
+ @Throws(IOException::class)
+ override fun rename(name: String, replacement: String) {
+ Log.d(TAG, "Renaming configuration for tunnel $name to $replacement")
+ val file = fileFor(name)
+ val replacementFile = fileFor(replacement)
+ if (!replacementFile.createNewFile()) throw IOException(context.getString(R.string.config_exists_error, replacement))
+ if (!file.renameTo(replacementFile)) {
+ if (!replacementFile.delete()) Log.w(TAG, "Couldn't delete marker file for new name $replacement")
+ throw IOException(context.getString(R.string.config_rename_error, file.name))
+ }
+ }
+
+ @Throws(IOException::class)
+ override fun save(name: String, config: Config): Config {
+ Log.d(TAG, "Saving configuration for tunnel $name")
+ val file = fileFor(name)
+ if (!file.isFile)
+ throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
+ FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
+ return config
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/FileConfigStore"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
new file mode 100644
index 00000000..afba41cb
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.databinding
+
+import android.text.InputFilter
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ObservableList
+import androidx.databinding.ViewDataBinding
+import androidx.databinding.adapters.ListenerUtil
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.wireguard.android.BR
+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
+import java.util.Optional
+
+/**
+ * Static methods for use by generated code in the Android data binding library.
+ */
+object BindingAdapters {
+ @JvmStatic
+ @BindingAdapter("checked")
+ fun setChecked(view: ToggleSwitch, checked: Boolean) {
+ view.setCheckedInternal(checked)
+ }
+
+ @JvmStatic
+ @BindingAdapter("filter")
+ fun setFilter(view: TextView, filter: InputFilter) {
+ view.filters = arrayOf(filter)
+ }
+
+ @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?
+ ) {
+ if (oldList === newList && oldLayoutId == newLayoutId)
+ return
+ var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
+ // If the layout changes, any existing listener must be replaced.
+ if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
+ listener.setList(null)
+ listener = null
+ // Stop tracking the old listener.
+ ListenerUtil.trackListener<Any?>(view, null, R.id.item_change_listener)
+ }
+ // Avoid adding a listener when there is no new list or layout.
+ if (newList == null || newLayoutId == 0)
+ return
+ if (listener == null) {
+ listener = ItemChangeListener(view, newLayoutId, newFragment)
+ ListenerUtil.trackListener(view, listener, R.id.item_change_listener)
+ }
+ // Either the list changed, or this is an entirely new listener because the layout changed.
+ listener.setList(newList)
+ }
+
+ @JvmStatic
+ @BindingAdapter("items", "layout")
+ fun <E> setItems(
+ view: LinearLayout,
+ oldList: Iterable<E>?, oldLayoutId: Int,
+ newList: Iterable<E>?, newLayoutId: Int
+ ) {
+ if (oldList === newList && oldLayoutId == newLayoutId)
+ return
+ view.removeAllViews()
+ if (newList == null)
+ return
+ val layoutInflater = LayoutInflater.from(view.context)
+ for (item in newList) {
+ val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, newLayoutId, view, false)
+ binding.setVariable(BR.collection, newList)
+ binding.setVariable(BR.item, item)
+ binding.executePendingBindings()
+ view.addView(binding.root)
+ }
+ }
+
+ @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<*, *>?
+ ) {
+ if (view.layoutManager == null)
+ view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
+ if (oldList === newList && oldLayoutId == newLayoutId)
+ return
+ // The ListAdapter interface is not generic, so this cannot be checked.
+ @Suppress("UNCHECKED_CAST") var adapter = view.adapter as? ObservableKeyedRecyclerViewAdapter<K, E>?
+ // If the layout changes, any existing adapter must be replaced.
+ if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
+ adapter.setList(null)
+ adapter = null
+ }
+ // Avoid setting an adapter when there is no new list or layout.
+ if (newList == null || newLayoutId == 0)
+ return
+ if (adapter == null) {
+ adapter = ObservableKeyedRecyclerViewAdapter(view.context, newLayoutId, newList)
+ view.adapter = adapter
+ }
+ adapter.setRowConfigurationHandler(newRowConfigurationHandler)
+ // Either the list changed, or this is an entirely new listener because the layout changed.
+ adapter.setList(newList)
+ }
+
+ @JvmStatic
+ @BindingAdapter("onBeforeCheckedChanged")
+ fun setOnBeforeCheckedChanged(
+ view: ToggleSwitch,
+ listener: OnBeforeCheckedChangeListener?
+ ) {
+ view.setOnBeforeCheckedChangeListener(listener)
+ }
+
+ @JvmStatic
+ @BindingAdapter("onFocusChange")
+ fun setOnFocusChange(
+ view: EditText,
+ listener: View.OnFocusChangeListener?
+ ) {
+ view.onFocusChangeListener = listener
+ }
+
+ @JvmStatic
+ @BindingAdapter("android:text")
+ fun setOptionalText(view: TextView, text: Optional<*>?) {
+ view.text = text?.map { it.toString() }?.orElse("") ?: ""
+ }
+
+ @JvmStatic
+ @BindingAdapter("android:text")
+ fun setInetNetworkSetText(view: TextView, networks: Iterable<InetNetwork?>?) {
+ view.text = if (networks != null) Attribute.join(networks) else ""
+ }
+
+ @JvmStatic
+ @BindingAdapter("android:text")
+ fun setInetAddressSetText(view: TextView, addresses: Iterable<InetAddress?>?) {
+ view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else ""
+ }
+
+ @JvmStatic
+ @BindingAdapter("android:text")
+ fun setStringSetText(view: TextView, strings: Iterable<String?>?) {
+ view.text = if (strings != null) Attribute.join(strings) else ""
+ }
+
+ @JvmStatic
+ fun tryParseInt(s: String?): Int {
+ if (s == null)
+ return 0
+ return try {
+ Integer.parseInt(s)
+ } catch (_: Throwable) {
+ 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
new file mode 100644
index 00000000..da153bbe
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/databinding/ItemChangeListener.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.databinding
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ObservableList
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.Fragment
+import com.wireguard.android.BR
+import java.lang.ref.WeakReference
+
+/**
+ * Helper class for binding an ObservableList to the children of a ViewGroup.
+ */
+internal class ItemChangeListener<T>(private val container: ViewGroup, private val layoutId: Int, private val fragment: Fragment?) {
+ private val callback = OnListChangedCallback(this)
+ private val layoutInflater: LayoutInflater = LayoutInflater.from(container.context)
+ private var list: ObservableList<T>? = null
+
+ private fun getView(position: Int, convertView: View?): View {
+ var binding = if (convertView != null) DataBindingUtil.getBinding<ViewDataBinding>(convertView) else null
+ if (binding == null) {
+ binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false)
+ }
+ require(list != null) { "Trying to get a view while list is still null" }
+ binding!!.setVariable(BR.collection, list)
+ binding.setVariable(BR.item, list!![position])
+ binding.setVariable(BR.fragment, fragment)
+ binding.executePendingBindings()
+ return binding.root
+ }
+
+ fun setList(newList: ObservableList<T>?) {
+ list?.removeOnListChangedCallback(callback)
+ list = newList
+ if (list != null) {
+ list!!.addOnListChangedCallback(callback)
+ callback.onChanged(list!!)
+ } else {
+ container.removeAllViews()
+ }
+ }
+
+ private class OnListChangedCallback<T> constructor(listener: ItemChangeListener<T>) : ObservableList.OnListChangedCallback<ObservableList<T>>() {
+ private val weakListener: WeakReference<ItemChangeListener<T>> = WeakReference(listener)
+
+ override fun onChanged(sender: ObservableList<T>) {
+ val listener = weakListener.get()
+ if (listener != null) {
+ // TODO: recycle views
+ listener.container.removeAllViews()
+ for (i in sender.indices)
+ listener.container.addView(listener.getView(i, null))
+ } else {
+ sender.removeOnListChangedCallback(this)
+ }
+ }
+
+ override fun onItemRangeChanged(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
+ val listener = weakListener.get()
+ if (listener != null) {
+ for (i in positionStart until positionStart + itemCount) {
+ val child = listener.container.getChildAt(i)
+ listener.container.removeViewAt(i)
+ listener.container.addView(listener.getView(i, child))
+ }
+ } else {
+ sender.removeOnListChangedCallback(this)
+ }
+ }
+
+ override fun onItemRangeInserted(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
+ val listener = weakListener.get()
+ if (listener != null) {
+ for (i in positionStart until positionStart + itemCount)
+ listener.container.addView(listener.getView(i, null))
+ } else {
+ sender.removeOnListChangedCallback(this)
+ }
+ }
+
+ override fun onItemRangeMoved(
+ sender: ObservableList<T>, fromPosition: Int,
+ toPosition: Int, itemCount: Int
+ ) {
+ val listener = weakListener.get()
+ if (listener != null) {
+ val views = arrayOfNulls<View>(itemCount)
+ for (i in 0 until itemCount) views[i] = listener.container.getChildAt(fromPosition + i)
+ listener.container.removeViews(fromPosition, itemCount)
+ for (i in 0 until itemCount) listener.container.addView(views[i], toPosition + i)
+ } else {
+ sender.removeOnListChangedCallback(this)
+ }
+ }
+
+ override fun onItemRangeRemoved(
+ sender: ObservableList<T>, positionStart: Int,
+ itemCount: Int
+ ) {
+ val listener = weakListener.get()
+ if (listener != null) {
+ listener.container.removeViews(positionStart, itemCount)
+ } else {
+ sender.removeOnListChangedCallback(this)
+ }
+ }
+
+ }
+
+}
diff --git a/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt b/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
new file mode 100644
index 00000000..f91581d0
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.databinding
+
+/**
+ * Interface for objects that have a identifying key of the given type.
+ */
+interface Keyed<K> {
+ val key: K
+}
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt
new file mode 100644
index 00000000..947644b3
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.databinding
+
+import androidx.databinding.ObservableArrayList
+
+/**
+ * ArrayList that allows looking up elements by some key property. As the key property must always
+ * be retrievable, this list cannot hold `null` elements. Because this class places no
+ * restrictions on the order or duplication of keys, lookup by key, as well as all list modification
+ * operations, require O(n) time.
+ */
+open class ObservableKeyedArrayList<K, E : Keyed<out K>> : ObservableArrayList<E>() {
+ fun containsKey(key: K) = indexOfKey(key) >= 0
+
+ operator fun get(key: K): E? {
+ val index = indexOfKey(key)
+ return if (index >= 0) get(index) else null
+ }
+
+ open fun indexOfKey(key: K): Int {
+ val iterator = listIterator()
+ while (iterator.hasNext()) {
+ val index = iterator.nextIndex()
+ if (iterator.next()!!.key == key)
+ return index
+ }
+ return -1
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt
new file mode 100644
index 00000000..91223ad1
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.databinding
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ObservableList
+import androidx.databinding.ViewDataBinding
+import androidx.recyclerview.widget.RecyclerView
+import com.wireguard.android.BR
+import java.lang.ref.WeakReference
+
+/**
+ * A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
+ */
+class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(
+ context: Context, private val layoutId: Int,
+ list: ObservableKeyedArrayList<K, E>?
+) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
+ private val callback = OnListChangedCallback(this)
+ private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
+ private var list: ObservableKeyedArrayList<K, E>? = null
+ private var rowConfigurationHandler: RowConfigurationHandler<ViewDataBinding, Any>? = null
+
+ private fun getItem(position: Int): E? = if (list == null || position < 0 || position >= list!!.size) null else list?.get(position)
+
+ override fun getItemCount() = list?.size ?: 0
+
+ override fun getItemId(position: Int) = (getKey(position)?.hashCode() ?: -1).toLong()
+
+ private fun getKey(position: Int): K? = getItem(position)?.key
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.binding.setVariable(BR.collection, list)
+ holder.binding.setVariable(BR.key, getKey(position))
+ holder.binding.setVariable(BR.item, getItem(position))
+ holder.binding.executePendingBindings()
+ if (rowConfigurationHandler != null) {
+ val item = getItem(position)
+ if (item != null) {
+ rowConfigurationHandler?.onConfigureRow(holder.binding, item, position)
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false))
+
+ fun setList(newList: ObservableKeyedArrayList<K, E>?) {
+ list?.removeOnListChangedCallback(callback)
+ list = newList
+ list?.addOnListChangedCallback(callback)
+ notifyDataSetChanged()
+ }
+
+ fun setRowConfigurationHandler(rowConfigurationHandler: RowConfigurationHandler<*, *>?) {
+ @Suppress("UNCHECKED_CAST")
+ this.rowConfigurationHandler = rowConfigurationHandler as? RowConfigurationHandler<ViewDataBinding, Any>
+ }
+
+ interface RowConfigurationHandler<B : ViewDataBinding, T> {
+ fun onConfigureRow(binding: B, item: T, position: Int)
+ }
+
+ private class OnListChangedCallback<E : Keyed<*>> constructor(adapter: ObservableKeyedRecyclerViewAdapter<*, E>) : ObservableList.OnListChangedCallback<ObservableList<E>>() {
+ private val weakAdapter: WeakReference<ObservableKeyedRecyclerViewAdapter<*, E>> = WeakReference(adapter)
+
+ override fun onChanged(sender: ObservableList<E>) {
+ val adapter = weakAdapter.get()
+ if (adapter != null)
+ adapter.notifyDataSetChanged()
+ else
+ sender.removeOnListChangedCallback(this)
+ }
+
+ override fun onItemRangeChanged(sender: ObservableList<E>, positionStart: Int,
+ itemCount: Int) {
+ onChanged(sender)
+ }
+
+ override fun onItemRangeInserted(sender: ObservableList<E>, positionStart: Int,
+ itemCount: Int) {
+ onChanged(sender)
+ }
+
+ override fun onItemRangeMoved(sender: ObservableList<E>, fromPosition: Int,
+ toPosition: Int, itemCount: Int) {
+ onChanged(sender)
+ }
+
+ override fun onItemRangeRemoved(sender: ObservableList<E>, positionStart: Int,
+ itemCount: Int) {
+ onChanged(sender)
+ }
+
+ }
+
+ class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
+
+ init {
+ setList(list)
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt b/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt
new file mode 100644
index 00000000..a09d726f
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.databinding
+
+import java.util.AbstractList
+import java.util.Collections
+import java.util.Comparator
+import java.util.Spliterator
+
+/**
+ * KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
+ * binary search to improve lookup and replacement times to O(log(n)). However, due to the
+ * array-based nature of this class, insertion and removal of elements with anything but the largest
+ * key still require O(n) time.
+ */
+class ObservableSortedKeyedArrayList<K, E : Keyed<out K>>(private val comparator: Comparator<in K>) : ObservableKeyedArrayList<K, E>() {
+ @Transient
+ private val keyList = KeyList(this)
+
+ override fun add(element: E): Boolean {
+ val insertionPoint = getInsertionPoint(element)
+ if (insertionPoint < 0) {
+ // Skipping insertion is non-destructive if the new and existing objects are the same.
+ if (element === get(-insertionPoint - 1)) return false
+ throw IllegalArgumentException("Element with same key already exists in list")
+ }
+ super.add(insertionPoint, element)
+ return true
+ }
+
+ override fun add(index: Int, element: E) {
+ val insertionPoint = getInsertionPoint(element)
+ require(insertionPoint >= 0) { "Element with same key already exists in list" }
+ if (insertionPoint != index) throw IndexOutOfBoundsException("Wrong index given for element")
+ super.add(index, element)
+ }
+
+ override fun addAll(elements: Collection<E>): Boolean {
+ var didChange = false
+ for (e in elements) {
+ if (add(e))
+ didChange = true
+ }
+ return didChange
+ }
+
+ override fun addAll(index: Int, elements: Collection<E>): Boolean {
+ var i = index
+ for (e in elements)
+ add(i++, e)
+ return true
+ }
+
+ private fun getInsertionPoint(e: E) = -Collections.binarySearch(keyList, e.key, comparator) - 1
+
+ override fun indexOfKey(key: K): Int {
+ val index = Collections.binarySearch(keyList, key, comparator)
+ return if (index >= 0) index else -1
+ }
+
+ override fun set(index: Int, element: E): E {
+ val order = comparator.compare(element.key, get(index).key)
+ if (order != 0) {
+ // Allow replacement if the new key would be inserted adjacent to the replaced element.
+ val insertionPoint = getInsertionPoint(element)
+ if (insertionPoint < index || insertionPoint > index + 1)
+ throw IndexOutOfBoundsException("Wrong index given for element")
+ }
+ return super.set(index, element)
+ }
+
+ private class KeyList<K, E : Keyed<out K>>(private val list: ObservableSortedKeyedArrayList<K, E>) : AbstractList<K>(), Set<K> {
+ override fun get(index: Int): K = list[index].key
+
+ override val size
+ get() = list.size
+
+ override fun spliterator(): Spliterator<K> = super<AbstractList>.spliterator()
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
new file mode 100644
index 00000000..78b9f3d7
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.content.pm.PackageManager
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.FrameLayout
+import androidx.core.os.bundleOf
+import androidx.fragment.app.setFragmentResult
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.wireguard.android.R
+import com.wireguard.android.util.resolveAttribute
+
+class AddTunnelsSheet : BottomSheetDialogFragment() {
+
+ private var behavior: BottomSheetBehavior<FrameLayout>? = null
+ private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ }
+
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
+ dismiss()
+ }
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ if (savedInstanceState != null) dismiss()
+ val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
+ if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) != true) {
+ val qrcode = view.findViewById<View>(R.id.create_from_qrcode)
+ qrcode.isEnabled = false
+ qrcode.visibility = View.GONE
+ }
+ return view
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ view.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ val dialog = dialog as BottomSheetDialog? ?: return
+ behavior = dialog.behavior
+ behavior?.apply {
+ state = BottomSheetBehavior.STATE_EXPANDED
+ peekHeight = 0
+ addBottomSheetCallback(bottomSheetCallback)
+ }
+ dialog.findViewById<View>(R.id.create_empty)?.setOnClickListener {
+ dismiss()
+ onRequestCreateConfig()
+ }
+ dialog.findViewById<View>(R.id.create_from_file)?.setOnClickListener {
+ dismiss()
+ onRequestImportConfig()
+ }
+ dialog.findViewById<View>(R.id.create_from_qrcode)?.setOnClickListener {
+ dismiss()
+ onRequestScanQRCode()
+ }
+ }
+ })
+ val gradientDrawable = GradientDrawable().apply {
+ setColor(requireContext().resolveAttribute(com.google.android.material.R.attr.colorSurface))
+ }
+ view.background = gradientDrawable
+ }
+
+ override fun dismiss() {
+ super.dismiss()
+ behavior?.removeBottomSheetCallback(bottomSheetCallback)
+ }
+
+ private fun onRequestCreateConfig() {
+ setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
+ }
+
+ private fun onRequestImportConfig() {
+ setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
+ }
+
+ private fun onRequestScanQRCode() {
+ setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
+ }
+
+ companion object {
+ const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
+ const val REQUEST_METHOD = "request_method"
+ const val REQUEST_CREATE = "request_create"
+ const val REQUEST_IMPORT = "request_import"
+ const val REQUEST_SCAN = "request_scan"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
new file mode 100644
index 00000000..1cd19934
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt
@@ -0,0 +1,170 @@
+/*
+ * 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
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.bundleOf
+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
+import com.wireguard.android.databinding.AppListDialogFragmentBinding
+import com.wireguard.android.databinding.ObservableKeyedArrayList
+import com.wireguard.android.model.ApplicationData
+import com.wireguard.android.util.ErrorMessages
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class AppListDialogFragment : DialogFragment() {
+ private val appData = ObservableKeyedArrayList<String, ApplicationData>()
+ private var currentlySelectedApps = emptyList<String>()
+ private var initiallyExcluded = false
+ private var button: Button? = null
+ private var tabs: TabLayout? = null
+
+ private fun loadData() {
+ val activity = activity ?: return
+ val pm = activity.packageManager
+ lifecycleScope.launch(Dispatchers.Default) {
+ try {
+ val applicationData: MutableList<ApplicationData> = ArrayList()
+ withContext(Dispatchers.IO) {
+ 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))
+ applicationData.add(appData)
+ appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
+ override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
+ if (propertyId == BR.selected)
+ setButtonText()
+ }
+ })
+ }
+ }
+ applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
+ withContext(Dispatchers.Main.immediate) {
+ appData.clear()
+ appData.addAll(applicationData)
+ setButtonText()
+ }
+ } catch (e: Throwable) {
+ withContext(Dispatchers.Main.immediate) {
+ val error = ErrorMessages[e]
+ val message = activity.getString(R.string.error_fetching_apps, error)
+ Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
+ dismissAllowingStateLoss()
+ }
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
+ 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)
+ getString(R.string.use_all_applications)
+ else when (tabs?.selectedTabPosition) {
+ 0 -> resources.getQuantityString(R.plurals.exclude_n_applications, numSelected, numSelected)
+ 1 -> resources.getQuantityString(R.plurals.include_n_applications, numSelected, numSelected)
+ else -> null
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val alertDialogBuilder = MaterialAlertDialogBuilder(requireActivity())
+ val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
+ binding.executePendingBindings()
+ alertDialogBuilder.setView(binding.root)
+ tabs = binding.tabs
+ tabs?.apply {
+ selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1))
+ addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabReselected(tab: TabLayout.Tab?) = Unit
+ override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
+ override fun onTabSelected(tab: TabLayout.Tab?) = setButtonText()
+ })
+ }
+ alertDialogBuilder.setPositiveButton(" ") { _, _ -> setSelectionAndDismiss() }
+ alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
+ alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
+ binding.fragment = this
+ binding.appData = appData
+ loadData()
+ val dialog = alertDialogBuilder.create()
+ dialog.setOnShowListener {
+ button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
+ setButtonText()
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ ->
+ val selectAll = appData.none { it.isSelected }
+ appData.forEach {
+ it.isSelected = selectAll
+ }
+ }
+ }
+ return dialog
+ }
+
+ private fun setSelectionAndDismiss() {
+ val selectedApps: MutableList<String> = ArrayList()
+ for (data in appData) {
+ if (data.isSelected) {
+ selectedApps.add(data.packageName)
+ }
+ }
+ setFragmentResult(
+ REQUEST_SELECTION, bundleOf(
+ KEY_SELECTED_APPS to selectedApps.toTypedArray(),
+ KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
+ )
+ )
+ dismiss()
+ }
+
+ companion object {
+ const val KEY_SELECTED_APPS = "selected_apps"
+ const val KEY_IS_EXCLUDED = "is_excluded"
+ const val REQUEST_SELECTION = "request_selection"
+
+ fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment {
+ val extras = Bundle()
+ extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
+ extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
+ val fragment = AppListDialogFragment()
+ fragment.arguments = extras
+ return fragment
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
new file mode 100644
index 00000000..d5c1723f
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.snackbar.Snackbar
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.activity.BaseActivity
+import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
+import com.wireguard.android.backend.GoBackend
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.TunnelDetailFragmentBinding
+import com.wireguard.android.databinding.TunnelListItemBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.ErrorMessages
+import kotlinx.coroutines.launch
+
+/**
+ * Base class for fragments that need to know the currently-selected tunnel. Only does anything when
+ * attached to a `BaseActivity`.
+ */
+abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
+ private var pendingTunnel: ObservableTunnel? = null
+ private var pendingTunnelUp: Boolean? = null
+ private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ val tunnel = pendingTunnel
+ val checked = pendingTunnelUp
+ if (tunnel != null && checked != null)
+ setTunnelStateWithPermissionsResult(tunnel, checked)
+ pendingTunnel = null
+ pendingTunnelUp = null
+ }
+
+ protected var selectedTunnel: ObservableTunnel?
+ get() = (activity as? BaseActivity)?.selectedTunnel
+ protected set(tunnel) {
+ (activity as? BaseActivity)?.selectedTunnel = tunnel
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ (activity as? BaseActivity)?.addOnSelectedTunnelChangedListener(this)
+ }
+
+ override fun onDetach() {
+ (activity as? BaseActivity)?.removeOnSelectedTunnelChangedListener(this)
+ super.onDetach()
+ }
+
+ fun setTunnelState(view: View, checked: Boolean) {
+ val tunnel = when (val binding = DataBindingUtil.findBinding<ViewDataBinding>(view)) {
+ is TunnelDetailFragmentBinding -> binding.tunnel
+ is TunnelListItemBinding -> binding.item
+ else -> return
+ } ?: return
+ val activity = activity ?: return
+ activity.lifecycleScope.launch {
+ if (Application.getBackend() is GoBackend) {
+ 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)
+ }
+ }
+
+ private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
+ val activity = activity ?: return
+ activity.lifecycleScope.launch {
+ try {
+ tunnel.setStateAsync(Tunnel.State.of(checked))
+ } catch (e: Throwable) {
+ val error = ErrorMessages[e]
+ val messageResId = if (checked) R.string.error_up else R.string.error_down
+ val message = activity.getString(messageResId, error)
+ val view = view
+ if (view != null)
+ Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+ .setAnchorView(view.findViewById(R.id.create_fab))
+ .show()
+ else
+ Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
+ Log.e(TAG, message, e)
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/BaseFragment"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
new file mode 100644
index 00000000..34c96505
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.os.Bundle
+import android.view.WindowManager
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Config
+import kotlinx.coroutines.launch
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+
+class ConfigNamingDialogFragment : DialogFragment() {
+ private var binding: ConfigNamingDialogFragmentBinding? = null
+ private var config: Config? = null
+
+ private fun createTunnelAndDismiss() {
+ val binding = binding ?: return
+ val activity = activity ?: return
+ val name = binding.tunnelNameText.text.toString()
+ activity.lifecycleScope.launch {
+ try {
+ Application.getTunnelManager().create(name, config)
+ dismiss()
+ } catch (e: Throwable) {
+ binding.tunnelNameTextLayout.error = e.message
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val configText = requireArguments().getString(KEY_CONFIG_TEXT)
+ val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
+ config = try {
+ Config.parse(ByteArrayInputStream(configBytes))
+ } catch (e: Throwable) {
+ when (e) {
+ is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
+ else -> throw e
+ }
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val activity = requireActivity()
+ 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) { _, _ -> createTunnelAndDismiss() }
+ alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
+ 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"
+
+ fun newInstance(configText: String?): ConfigNamingDialogFragment {
+ val extras = Bundle()
+ extras.putString(KEY_CONFIG_TEXT, configText)
+ val fragment = ConfigNamingDialogFragment()
+ fragment.arguments = extras
+ return fragment
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
new file mode 100644
index 00000000..81d8e8c6
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+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
+import com.wireguard.android.databinding.TunnelDetailFragmentBinding
+import com.wireguard.android.databinding.TunnelDetailPeerBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.QuantityFormatter
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * Fragment that shows details about a specific tunnel.
+ */
+class TunnelDetailFragment : BaseFragment(), MenuProvider {
+ private var binding: TunnelDetailFragmentBinding? = null
+ private var lastState = Tunnel.State.TOGGLE
+ private var timerActive = true
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ return false
+ }
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.tunnel_detail, menu)
+ }
+
+ 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()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ timerActive = true
+ lifecycleScope.launch {
+ while (timerActive) {
+ updateStats()
+ delay(1000)
+ }
+ }
+ }
+
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
+ val binding = binding ?: return
+ binding.tunnel = newTunnel
+ if (newTunnel == null) {
+ binding.config = null
+ } else {
+ lifecycleScope.launch {
+ try {
+ binding.config = newTunnel.getConfigAsync()
+ } catch (_: Throwable) {
+ binding.config = null
+ }
+ }
+ }
+ lastState = Tunnel.State.TOGGLE
+ lifecycleScope.launch { updateStats() }
+ }
+
+ override fun onStop() {
+ timerActive = false
+ super.onStop()
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ binding ?: return
+ binding!!.fragment = this
+ onSelectedTunnelChanged(null, selectedTunnel)
+ super.onViewStateRestored(savedInstanceState)
+ }
+
+ private suspend fun updateStats() {
+ val binding = binding ?: return
+ val tunnel = binding.tunnel ?: return
+ if (!isResumed) return
+ val state = tunnel.state
+ if (state != Tunnel.State.UP && lastState == state) return
+ lastState = state
+ try {
+ val statistics = tunnel.getStatisticsAsync()
+ for (i in 0 until binding.peersLayout.childCount) {
+ val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
+ ?: continue
+ val publicKey = peer.item!!.publicKey
+ val peerStats = statistics.peer(publicKey)
+ if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
+ peer.transferLabel.visibility = View.GONE
+ peer.transferText.visibility = View.GONE
+ } 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
+ }
+ }
+ } catch (e: Throwable) {
+ for (i in 0 until binding.peersLayout.childCount) {
+ val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
+ ?: continue
+ peer.transferLabel.visibility = View.GONE
+ peer.transferText.visibility = View.GONE
+ 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
new file mode 100644
index 00000000..edf4b226
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -0,0 +1,333 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.content.Context
+import android.os.Bundle
+import android.text.InputType
+import android.util.Log
+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 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
+import com.wireguard.android.R
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.TunnelEditorFragmentBinding
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.AdminKnobs
+import com.wireguard.android.util.BiometricAuthenticator
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.viewmodel.ConfigProxy
+import com.wireguard.config.Config
+import kotlinx.coroutines.launch
+
+/**
+ * Fragment for editing a WireGuard configuration.
+ */
+class TunnelEditorFragment : BaseFragment(), MenuProvider {
+ private var haveShownKeys = false
+ private var binding: TunnelEditorFragmentBinding? = null
+ private var tunnel: ObservableTunnel? = null
+
+ private fun onConfigLoaded(config: Config) {
+ binding?.config = ConfigProxy(config)
+ }
+
+ private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
+ val ctx = activity ?: Application.get()
+ if (throwable == null) {
+ val message = ctx.getString(R.string.config_save_success, savedTunnel.name)
+ Log.d(TAG, message)
+ Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
+ onFinished()
+ } else {
+ val error = ErrorMessages[throwable]
+ val message = ctx.getString(R.string.config_save_error, savedTunnel.name, error)
+ Log.e(TAG, message, throwable)
+ val binding = binding
+ if (binding != null)
+ Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
+ else
+ Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
+ menuInflater.inflate(R.menu.config_editor, menu)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
+ binding?.apply {
+ executePendingBindings()
+ privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
+ }
+ 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
+ super.onDestroyView()
+ }
+
+ private fun onFinished() {
+ // Hide the keyboard; it rarely goes away on its own.
+ val activity = activity ?: return
+ 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
+ )
+ }
+ parentFragmentManager.popBackStackImmediate()
+
+ // If we just made a new one, save it to select the details page.
+ if (selectedTunnel != tunnel)
+ selectedTunnel = tunnel
+ }
+
+ override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
+ if (menuItem.itemId == R.id.menu_action_save) {
+ binding ?: return false
+ val newConfig = try {
+ binding!!.config!!.resolve()
+ } catch (e: Throwable) {
+ val error = ErrorMessages[e]
+ val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
+ val message = getString(R.string.config_save_error, tunnelName, error)
+ Log.e(TAG, message, e)
+ Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
+ return false
+ }
+ val activity = requireActivity()
+ activity.lifecycleScope.launch {
+ when {
+ tunnel == null -> {
+ Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
+ val manager = Application.getTunnelManager()
+ try {
+ onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
+ } catch (e: Throwable) {
+ onTunnelCreated(null, e)
+ }
+ }
+
+ tunnel!!.name != binding!!.name -> {
+ Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
+ try {
+ tunnel!!.setNameAsync(binding!!.name!!)
+ onTunnelRenamed(tunnel!!, newConfig, null)
+ } catch (e: Throwable) {
+ onTunnelRenamed(tunnel!!, newConfig, e)
+ }
+ }
+
+ else -> {
+ Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
+ try {
+ tunnel!!.setConfigAsync(newConfig)
+ onConfigSaved(tunnel!!, null)
+ } catch (e: Throwable) {
+ onConfigSaved(tunnel!!, e)
+ }
+ }
+ }
+ }
+ return true
+ }
+ return false
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun onRequestSetExcludedIncludedApplications(view: View?) {
+ if (binding != null) {
+ var isExcluded = true
+ var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
+ if (selectedApps.isEmpty()) {
+ selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
+ if (selectedApps.isNotEmpty())
+ isExcluded = false
+ }
+ val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded)
+ childFragmentManager.setFragmentResultListener(AppListDialogFragment.REQUEST_SELECTION, viewLifecycleOwner) { _, bundle ->
+ requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
+ val newSelections = requireNotNull(bundle.getStringArray(AppListDialogFragment.KEY_SELECTED_APPS))
+ val excluded = requireNotNull(bundle.getBoolean(AppListDialogFragment.KEY_IS_EXCLUDED))
+ if (excluded) {
+ binding!!.config!!.`interface`.includedApplications.clear()
+ binding!!.config!!.`interface`.excludedApplications.apply {
+ clear()
+ addAll(newSelections)
+ }
+ } else {
+ binding!!.config!!.`interface`.excludedApplications.clear()
+ binding!!.config!!.`interface`.includedApplications.apply {
+ clear()
+ addAll(newSelections)
+ }
+ }
+ }
+ fragment.show(childFragmentManager, null)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ if (binding != null) outState.putParcelable(KEY_LOCAL_CONFIG, binding!!.config)
+ outState.putString(KEY_ORIGINAL_NAME, if (tunnel == null) null else tunnel!!.name)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onSelectedTunnelChanged(
+ oldTunnel: ObservableTunnel?,
+ newTunnel: ObservableTunnel?
+ ) {
+ tunnel = newTunnel
+ if (binding == null) return
+ binding!!.config = ConfigProxy()
+ if (tunnel != null) {
+ binding!!.name = tunnel!!.name
+ lifecycleScope.launch {
+ try {
+ onConfigLoaded(tunnel!!.getConfigAsync())
+ } catch (_: Throwable) {
+ }
+ }
+ } else {
+ binding!!.name = ""
+ }
+ }
+
+ private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
+ val ctx = activity ?: Application.get()
+ if (throwable == null) {
+ tunnel = newTunnel
+ val message = ctx.getString(R.string.tunnel_create_success, tunnel!!.name)
+ Log.d(TAG, message)
+ Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
+ onFinished()
+ } else {
+ val error = ErrorMessages[throwable]
+ val message = ctx.getString(R.string.tunnel_create_error, error)
+ Log.e(TAG, message, throwable)
+ val binding = binding
+ if (binding != null)
+ Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
+ else
+ Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private 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)
+ Log.d(TAG, message)
+ // Now save the rest of configuration changes.
+ Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
+ try {
+ renamedTunnel.setConfigAsync(newConfig)
+ onConfigSaved(renamedTunnel, null)
+ } catch (e: Throwable) {
+ onConfigSaved(renamedTunnel, e)
+ }
+ } else {
+ val error = ErrorMessages[throwable]
+ val message = ctx.getString(R.string.tunnel_rename_error, error)
+ Log.e(TAG, message, throwable)
+ val binding = binding
+ if (binding != null)
+ Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
+ else
+ Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ binding ?: return
+ binding!!.fragment = this
+ if (savedInstanceState == null) {
+ onSelectedTunnelChanged(null, selectedTunnel)
+ } else {
+ tunnel = selectedTunnel
+ 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
+ }
+ super.onViewStateRestored(savedInstanceState)
+ }
+
+ private var showingAuthenticator = false
+
+ fun onKeyClick(view: View) = onKeyFocusChange(view, true)
+
+ fun onKeyFocusChange(view: View, isFocused: Boolean) {
+ if (!isFocused || showingAuthenticator) return
+ val edit = view as? EditText ?: return
+ if (edit.inputType == InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) return
+ if (!haveShownKeys && edit.text.isNotEmpty()) {
+ if (AdminKnobs.disableConfigExport) return
+ showingAuthenticator = true
+ BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, this) {
+ showingAuthenticator = false
+ when (it) {
+ is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
+ haveShownKeys = true
+ showPrivateKey(edit)
+ }
+
+ is BiometricAuthenticator.Result.Failure -> {
+ Snackbar.make(
+ binding!!.mainContainer,
+ it.message,
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+
+ is BiometricAuthenticator.Result.Cancelled -> {}
+ }
+ }
+ } else {
+ showPrivateKey(edit)
+ }
+ }
+
+ private fun showPrivateKey(edit: EditText) {
+ activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ }
+
+ companion object {
+ private const val KEY_LOCAL_CONFIG = "local_config"
+ private const val KEY_ORIGINAL_NAME = "original_name"
+ private const val TAG = "WireGuard/TunnelEditorFragment"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
new file mode 100644
index 00000000..cba7c476
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
@@ -0,0 +1,342 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.fragment
+
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.widget.Toast
+import androidx.activity.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
+import com.wireguard.android.R
+import com.wireguard.android.activity.TunnelCreatorActivity
+import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
+import com.wireguard.android.databinding.TunnelListFragmentBinding
+import com.wireguard.android.databinding.TunnelListItemBinding
+import com.wireguard.android.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
+
+/**
+ * Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
+ */
+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 {
+ 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) }
+ }
+ }
+ }
+
+ private val qrImportResultLauncher = registerForActivityResult(ScanContract()) { result ->
+ val qrCode = result.contents
+ val activity = activity
+ if (qrCode != null && activity != null) {
+ activity.lifecycleScope.launch { TunnelImporter.importTunnel(parentFragmentManager, qrCode) { showSnackbar(it) } }
+ }
+ }
+
+ private val snackbarUpdateShower = SnackbarUpdateShower(this)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ if (savedInstanceState != null) {
+ val checkedItems = savedInstanceState.getIntegerArrayList(CHECKED_ITEMS)
+ if (checkedItems != null) {
+ for (i in checkedItems) actionModeListener.setItemChecked(i, true)
+ }
+ }
+ }
+
+ 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()
+ .setOrientationLocked(false)
+ .setBeepEnabled(false)
+ .setPrompt(getString(R.string.qr_code_hint))
+ )
+ }
+ }
+ }
+ bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET")
+ }
+ executePendingBindings()
+ snackbarUpdateShower.attach(mainContainer, createFab)
+ }
+ backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
+ backPressedCallback?.isEnabled = false
+
+ return binding?.root
+ }
+
+ override fun onDestroyView() {
+ binding = null
+ super.onDestroyView()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putIntegerArrayList(CHECKED_ITEMS, actionModeListener.getCheckedItems())
+ }
+
+ override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
+ binding ?: return
+ lifecycleScope.launch {
+ val tunnels = Application.getTunnelManager().getTunnels()
+ if (newTunnel != null) viewForTunnel(newTunnel, tunnels)?.setSingleSelected(true)
+ if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels)?.setSingleSelected(false)
+ }
+ }
+
+ private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
+ val message: String
+ val ctx = activity ?: Application.get()
+ if (throwable == null) {
+ message = ctx.resources.getQuantityString(R.plurals.delete_success, count, count)
+ } else {
+ val error = ErrorMessages[throwable]
+ message = ctx.resources.getQuantityString(R.plurals.delete_error, count, count, error)
+ Log.e(TAG, message, throwable)
+ }
+ showSnackbar(message)
+ }
+
+ override fun onViewStateRestored(savedInstanceState: Bundle?) {
+ super.onViewStateRestored(savedInstanceState)
+ binding ?: return
+ binding!!.fragment = this
+ lifecycleScope.launch { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
+ binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
+ override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
+ binding.fragment = this@TunnelListFragment
+ binding.root.setOnClickListener {
+ if (actionMode == null) {
+ selectedTunnel = item
+ } else {
+ actionModeListener.toggleItemChecked(position)
+ }
+ }
+ binding.root.setOnLongClickListener {
+ actionModeListener.toggleItemChecked(position)
+ true
+ }
+ if (actionMode != null)
+ (binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
+ else
+ (binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel == item)
+ }
+ }
+ }
+
+ private fun showSnackbar(message: CharSequence) {
+ val binding = binding
+ if (binding != null)
+ Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
+ .setAnchorView(binding.createFab)
+ .show()
+ else
+ Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
+ }
+
+ private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout? {
+ return binding?.tunnelList?.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))?.itemView as? MultiselectableRelativeLayout
+ }
+
+ private inner class ActionModeListener : ActionMode.Callback {
+ val checkedItems: MutableCollection<Int> = HashSet()
+ private var resources: Resources? = null
+
+ fun getCheckedItems(): ArrayList<Int> {
+ return ArrayList(checkedItems)
+ }
+
+ override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.menu_action_delete -> {
+ val activity = activity ?: return true
+ val copyCheckedItems = HashSet(checkedItems)
+ binding?.createFab?.apply {
+ visibility = View.VISIBLE
+ scaleX = 1f
+ scaleY = 1f
+ }
+ activity.lifecycleScope.launch {
+ try {
+ val tunnels = Application.getTunnelManager().getTunnels()
+ val tunnelsToDelete = ArrayList<ObservableTunnel>()
+ for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
+ val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
+ onTunnelDeletionFinished(futures.awaitAll().size, null)
+ } catch (e: Throwable) {
+ onTunnelDeletionFinished(0, e)
+ }
+ }
+ checkedItems.clear()
+ mode.finish()
+ true
+ }
+
+ R.id.menu_action_select_all -> {
+ lifecycleScope.launch {
+ val tunnels = Application.getTunnelManager().getTunnels()
+ for (i in 0 until tunnels.size) {
+ setItemChecked(i, true)
+ }
+ }
+ true
+ }
+
+ else -> false
+ }
+ }
+
+ override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+ actionMode = mode
+ backPressedCallback?.isEnabled = true
+ if (activity != null) {
+ resources = activity!!.resources
+ }
+ animateFab(binding?.createFab, false)
+ mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
+ binding?.tunnelList?.adapter?.notifyDataSetChanged()
+ return true
+ }
+
+ override fun onDestroyActionMode(mode: ActionMode) {
+ actionMode = null
+ backPressedCallback?.isEnabled = false
+ resources = null
+ animateFab(binding?.createFab, true)
+ checkedItems.clear()
+ binding?.tunnelList?.adapter?.notifyDataSetChanged()
+ }
+
+ override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+ updateTitle(mode)
+ return false
+ }
+
+ fun setItemChecked(position: Int, checked: Boolean) {
+ if (checked) {
+ checkedItems.add(position)
+ } else {
+ checkedItems.remove(position)
+ }
+ val adapter = if (binding == null) null else binding!!.tunnelList.adapter
+ if (actionMode == null && !checkedItems.isEmpty() && activity != null) {
+ (activity as AppCompatActivity).startSupportActionMode(this)
+ } else if (actionMode != null && checkedItems.isEmpty()) {
+ actionMode!!.finish()
+ }
+ adapter?.notifyItemChanged(position)
+ updateTitle(actionMode)
+ }
+
+ fun toggleItemChecked(position: Int) {
+ setItemChecked(position, !checkedItems.contains(position))
+ }
+
+ private fun updateTitle(mode: ActionMode?) {
+ if (mode == null) {
+ return
+ }
+ val count = checkedItems.size
+ if (count == 0) {
+ mode.title = ""
+ } else {
+ mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
+ }
+ }
+
+ private fun animateFab(view: View?, show: Boolean) {
+ view ?: return
+ val animation = AnimationUtils.loadAnimation(
+ context, if (show) R.anim.scale_up else R.anim.scale_down
+ )
+ animation.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationRepeat(animation: Animation?) {
+ }
+
+ override fun onAnimationEnd(animation: Animation?) {
+ if (!show) view.visibility = View.GONE
+ }
+
+ override fun onAnimationStart(animation: Animation?) {
+ if (show) view.visibility = View.VISIBLE
+ }
+ })
+ view.startAnimation(animation)
+ }
+ }
+
+ companion object {
+ private const val CHECKED_ITEMS = "CHECKED_ITEMS"
+ private const val TAG = "WireGuard/TunnelListFragment"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt b/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt
new file mode 100644
index 00000000..f3892424
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.model
+
+import android.graphics.drawable.Drawable
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.wireguard.android.BR
+import com.wireguard.android.databinding.Keyed
+
+class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> {
+ override val key = name
+
+ @get:Bindable
+ var isSelected = isSelected
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.selected)
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
new file mode 100644
index 00000000..aa237aee
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.model
+
+import android.util.Log
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.wireguard.android.BR
+import com.wireguard.android.backend.Statistics
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.databinding.Keyed
+import com.wireguard.android.util.applicationScope
+import com.wireguard.config.Config
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+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
+) : BaseObservable(), Keyed<String>, Tunnel {
+ override val key
+ get() = name
+
+ @Bindable
+ override fun getName() = name
+
+ suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
+ if (name != this@ObservableTunnel.name)
+ manager.setTunnelName(this@ObservableTunnel, name)
+ else
+ this@ObservableTunnel.name
+ }
+
+ fun onNameChanged(name: String): String {
+ this.name = name
+ notifyPropertyChanged(BR.name)
+ return name
+ }
+
+
+ @get:Bindable
+ var state = state
+ private set
+
+ override fun onStateChange(newState: Tunnel.State) {
+ onStateChanged(newState)
+ }
+
+ fun onStateChanged(state: Tunnel.State): Tunnel.State {
+ if (state != Tunnel.State.UP) onStatisticsChanged(null)
+ this.state = state
+ notifyPropertyChanged(BR.state)
+ return state
+ }
+
+ suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
+ if (state != this@ObservableTunnel.state)
+ manager.setTunnelState(this@ObservableTunnel, state)
+ else
+ this@ObservableTunnel.state
+ }
+
+
+ @get:Bindable
+ var config = config
+ get() {
+ if (field == null)
+ // Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
+ applicationScope.launch {
+ try {
+ manager.getTunnelConfig(this@ObservableTunnel)
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ return field
+ }
+ private set
+
+ suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
+ config ?: manager.getTunnelConfig(this@ObservableTunnel)
+ }
+
+ suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
+ this@ObservableTunnel.config.let {
+ if (config != it)
+ manager.setTunnelConfig(this@ObservableTunnel, config)
+ else
+ it
+ }
+ }
+
+ fun onConfigChanged(config: Config?): Config? {
+ this.config = config
+ notifyPropertyChanged(BR.config)
+ return config
+ }
+
+
+ @get:Bindable
+ var statistics: Statistics? = null
+ get() {
+ if (field == null || field?.isStale != false)
+ // Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
+ applicationScope.launch {
+ try {
+ manager.getTunnelStatistics(this@ObservableTunnel)
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ return field
+ }
+ private set
+
+ suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
+ statistics.let {
+ if (it == null || it.isStale)
+ manager.getTunnelStatistics(this@ObservableTunnel)
+ else
+ it
+ }
+ }
+
+ fun onStatisticsChanged(statistics: Statistics?): Statistics? {
+ this.statistics = statistics
+ notifyPropertyChanged(BR.statistics)
+ return statistics
+ }
+
+
+ suspend fun deleteAsync() = manager.delete(this)
+
+
+ companion object {
+ private const val TAG = "WireGuard/ObservableTunnel"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt b/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt
new file mode 100644
index 00000000..e6b46a54
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.model
+
+object TunnelComparator : Comparator<String> {
+ private class NaturalSortString(originalString: String) {
+ class NaturalSortToken(val maybeString: String?, val maybeNumber: Int?) : Comparable<NaturalSortToken> {
+ override fun compareTo(other: NaturalSortToken): Int {
+ if (maybeString == null) {
+ if (other.maybeString != null || maybeNumber!! < other.maybeNumber!!) {
+ return -1
+ } else if (maybeNumber > other.maybeNumber) {
+ return 1
+ }
+ } else if (other.maybeString == null || maybeString > other.maybeString) {
+ return 1
+ } else if (maybeString < other.maybeString) {
+ return -1
+ }
+ return 0
+ }
+ }
+
+ val tokens: MutableList<NaturalSortToken> = ArrayList()
+
+ init {
+ for (s in NATURAL_SORT_DIGIT_FINDER.findAll(originalString.split(WHITESPACE_FINDER).joinToString(" ").lowercase())) {
+ try {
+ val n = s.value.toInt()
+ tokens.add(NaturalSortToken(null, n))
+ } catch (_: NumberFormatException) {
+ tokens.add(NaturalSortToken(s.value, null))
+ }
+ }
+ }
+
+ private companion object {
+ private val NATURAL_SORT_DIGIT_FINDER = Regex("""\d+|\D+""")
+ private val WHITESPACE_FINDER = Regex("""\s""")
+ }
+ }
+
+ override fun compare(a: String, b: String): Int {
+ if (a == b)
+ return 0
+ val na = NaturalSortString(a)
+ val nb = NaturalSortString(b)
+ for (i in 0 until nb.tokens.size) {
+ if (i == na.tokens.size) {
+ return -1
+ }
+ val c = na.tokens[i].compareTo(nb.tokens[i])
+ if (c != 0)
+ return c
+ }
+ return 1
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
new file mode 100644
index 00000000..ba873fa6
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.model
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import android.widget.Toast
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import com.wireguard.android.Application.Companion.get
+import com.wireguard.android.Application.Companion.getBackend
+import com.wireguard.android.Application.Companion.getTunnelManager
+import com.wireguard.android.BR
+import com.wireguard.android.R
+import com.wireguard.android.backend.Statistics
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.configStore.ConfigStore
+import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.UserKnobs
+import com.wireguard.android.util.applicationScope
+import com.wireguard.config.Config
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Maintains and mediates changes to the set of available WireGuard tunnels,
+ */
+class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
+ private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
+ private val context: Context = get()
+ private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
+ private var haveLoaded = false
+
+ private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
+ val tunnel = ObservableTunnel(this, name, config, state)
+ tunnelMap.add(tunnel)
+ return tunnel
+ }
+
+ suspend fun getTunnels(): ObservableSortedKeyedArrayList<String, ObservableTunnel> = tunnels.await()
+
+ suspend fun create(name: String, config: Config?): ObservableTunnel = withContext(Dispatchers.Main.immediate) {
+ if (Tunnel.isNameInvalid(name))
+ throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
+ if (tunnelMap.containsKey(name))
+ throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
+ addToList(name, withContext(Dispatchers.IO) { configStore.create(name, config!!) }, Tunnel.State.DOWN)
+ }
+
+ suspend fun delete(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
+ val originalState = tunnel.state
+ val wasLastUsed = tunnel == lastUsedTunnel
+ // Make sure nothing touches the tunnel.
+ if (wasLastUsed)
+ lastUsedTunnel = null
+ tunnelMap.remove(tunnel)
+ try {
+ if (originalState == Tunnel.State.UP)
+ withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
+ try {
+ withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
+ } catch (e: Throwable) {
+ if (originalState == Tunnel.State.UP)
+ withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
+ throw e
+ }
+ } catch (e: Throwable) {
+ // Failure, put the tunnel back.
+ tunnelMap.add(tunnel)
+ if (wasLastUsed)
+ lastUsedTunnel = tunnel
+ throw e
+ }
+ }
+
+ @get:Bindable
+ var lastUsedTunnel: ObservableTunnel? = null
+ private set(value) {
+ if (value == field) return
+ field = value
+ notifyPropertyChanged(BR.lastUsedTunnel)
+ applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
+ }
+
+ suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
+ tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
+ }
+
+ fun onCreate() {
+ applicationScope.launch {
+ try {
+ onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ }
+
+ private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
+ for (name in present)
+ addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
+ applicationScope.launch {
+ val lastUsedName = UserKnobs.lastUsedTunnel.first()
+ if (lastUsedName != null)
+ lastUsedTunnel = tunnelMap[lastUsedName]
+ haveLoaded = true
+ restoreState(true)
+ tunnels.complete(tunnelMap)
+ }
+ }
+
+ private fun refreshTunnelStates() {
+ applicationScope.launch {
+ try {
+ val running = withContext(Dispatchers.IO) { getBackend().runningTunnelNames }
+ for (tunnel in tunnelMap)
+ tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN)
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ }
+
+ suspend fun restoreState(force: Boolean) {
+ if (!haveLoaded || (!force && !UserKnobs.restoreOnBoot.first()))
+ return
+ val previouslyRunning = UserKnobs.runningTunnels.first()
+ if (previouslyRunning.isEmpty()) return
+ withContext(Dispatchers.IO) {
+ try {
+ tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }
+ .awaitAll()
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ }
+
+ suspend fun saveState() {
+ UserKnobs.setRunningTunnels(tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet())
+ }
+
+ suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
+ tunnel.onConfigChanged(withContext(Dispatchers.IO) {
+ getBackend().setState(tunnel, tunnel.state, config)
+ configStore.save(tunnel.name, config)
+ })!!
+ }
+
+ suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
+ if (Tunnel.isNameInvalid(name))
+ throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
+ if (tunnelMap.containsKey(name)) {
+ throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
+ }
+ val originalState = tunnel.state
+ val wasLastUsed = tunnel == lastUsedTunnel
+ // Make sure nothing touches the tunnel.
+ if (wasLastUsed)
+ lastUsedTunnel = null
+ tunnelMap.remove(tunnel)
+ var throwable: Throwable? = null
+ var newName: String? = null
+ try {
+ if (originalState == Tunnel.State.UP)
+ withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
+ withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
+ newName = tunnel.onNameChanged(name)
+ if (originalState == Tunnel.State.UP)
+ withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
+ } catch (e: Throwable) {
+ throwable = e
+ // On failure, we don't know what state the tunnel might be in. Fix that.
+ getTunnelState(tunnel)
+ }
+ // Add the tunnel back to the manager, under whatever name it thinks it has.
+ tunnelMap.add(tunnel)
+ if (wasLastUsed)
+ lastUsedTunnel = tunnel
+ if (throwable != null)
+ throw throwable
+ newName!!
+ }
+
+ suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
+ var newState = tunnel.state
+ var throwable: Throwable? = null
+ try {
+ newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
+ if (newState == Tunnel.State.UP)
+ lastUsedTunnel = tunnel
+ } catch (e: Throwable) {
+ throwable = e
+ }
+ tunnel.onStateChanged(newState)
+ saveState()
+ if (throwable != null)
+ throw throwable
+ newState
+ }
+
+ class IntentReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ applicationScope.launch {
+ val manager = getTunnelManager()
+ if (intent == null) return@launch
+ val action = intent.action ?: return@launch
+ if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
+ manager.refreshTunnelStates()
+ return@launch
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !UserKnobs.allowRemoteControlIntents.first())
+ return@launch
+ val state: Tunnel.State
+ state = when (action) {
+ "com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
+ "com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
+ else -> return@launch
+ }
+ val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
+ val tunnels = manager.getTunnels()
+ val tunnel = tunnels[tunnelName] ?: return@launch
+ try {
+ manager.setTunnelState(tunnel, state)
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ }
+
+ suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
+ tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
+ }
+
+ suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
+ tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/TunnelManager"
+ }
+}
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
new file mode 100644
index 00000000..20de8e93
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.util.AttributeSet
+import android.util.Log
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.activity.SettingsActivity
+import com.wireguard.android.backend.Tunnel
+import com.wireguard.android.backend.WgQuickBackend
+import com.wireguard.android.util.UserKnobs
+import com.wireguard.android.util.activity
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.system.exitProcess
+
+class KernelModuleEnablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ private var state = State.UNKNOWN
+
+ init {
+ isVisible = false
+ lifecycleScope.launch {
+ setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
+ }
+ }
+
+ override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
+
+ override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
+
+ override fun onClick() {
+ activity.lifecycleScope.launch {
+ if (state == State.DISABLED) {
+ setState(State.ENABLING)
+ UserKnobs.setEnableKernelModule(true)
+ } else if (state == State.ENABLED) {
+ setState(State.DISABLING)
+ UserKnobs.setEnableKernelModule(false)
+ }
+ val observableTunnels = Application.getTunnelManager().getTunnels()
+ val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
+ try {
+ downings.awaitAll()
+ withContext(Dispatchers.IO) {
+ val restartIntent = Intent(context, SettingsActivity::class.java)
+ restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ Application.get().startActivity(restartIntent)
+ exitProcess(0)
+ }
+ } catch (e: Throwable) {
+ Log.e(TAG, Log.getStackTraceString(e))
+ }
+ }
+ }
+
+ private fun setState(state: State) {
+ if (this.state == state) return
+ this.state = state
+ if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
+ if (isVisible != state.visible) isVisible = state.visible
+ notifyChanged()
+ }
+
+ private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
+ UNKNOWN(0, 0, false, false),
+ ENABLED(R.string.module_enabler_enabled_title, R.string.module_enabler_enabled_summary, true, true),
+ DISABLED(R.string.module_enabler_disabled_title, R.string.module_enabler_disabled_summary, true, true),
+ ENABLING(R.string.module_enabler_disabled_title, R.string.success_application_will_restart, false, true),
+ DISABLING(R.string.module_enabler_enabled_title, R.string.success_application_will_restart, false, true);
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/KernelModuleEnablerPreference"
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
new file mode 100644
index 00000000..1a491684
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/PreferencesPreferenceDataStore.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.preference
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.floatPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.core.stringSetPreferencesKey
+import androidx.preference.PreferenceDataStore
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+class PreferencesPreferenceDataStore(private val coroutineScope: CoroutineScope, private val dataStore: DataStore<Preferences>) : PreferenceDataStore() {
+ override fun putString(key: String?, value: String?) {
+ if (key == null) return
+ val pk = stringPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ if (value == null) it.remove(pk)
+ else it[pk] = value
+ }
+ }
+ }
+
+ override fun putStringSet(key: String?, values: Set<String?>?) {
+ if (key == null) return
+ val pk = stringSetPreferencesKey(key)
+ val filteredValues = values?.filterNotNull()?.toSet()
+ coroutineScope.launch {
+ dataStore.edit {
+ if (filteredValues == null || filteredValues.isEmpty()) it.remove(pk)
+ else it[pk] = filteredValues
+ }
+ }
+ }
+
+ override fun putInt(key: String?, value: Int) {
+ if (key == null) return
+ val pk = intPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putLong(key: String?, value: Long) {
+ if (key == null) return
+ val pk = longPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putFloat(key: String?, value: Float) {
+ if (key == null) return
+ val pk = floatPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun putBoolean(key: String?, value: Boolean) {
+ if (key == null) return
+ val pk = booleanPreferencesKey(key)
+ coroutineScope.launch {
+ dataStore.edit {
+ it[pk] = value
+ }
+ }
+ }
+
+ override fun getString(key: String?, defValue: String?): String? {
+ if (key == null) return defValue
+ val pk = stringPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getStringSet(key: String?, defValues: Set<String?>?): Set<String?>? {
+ if (key == null) return defValues
+ val pk = stringSetPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValues }.first()
+ }
+ }
+
+ override fun getInt(key: String?, defValue: Int): Int {
+ if (key == null) return defValue
+ val pk = intPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getLong(key: String?, defValue: Long): Long {
+ if (key == null) return defValue
+ val pk = longPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getFloat(key: String?, defValue: Float): Float {
+ if (key == null) return defValue
+ val pk = floatPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+
+ override fun getBoolean(key: String?, defValue: Boolean): Boolean {
+ if (key == null) return defValue
+ val pk = booleanPreferencesKey(key)
+ return runBlocking {
+ dataStore.data.map { it[pk] ?: defValue }.first()
+ }
+ }
+}
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
new file mode 100644
index 00000000..dac80e88
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.util.AttributeSet
+import androidx.preference.Preference
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.util.ToolsInstaller
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
+ * result as the preference summary.
+ */
+class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ private var state = State.INITIAL
+ override fun getSummary() = context.getString(state.messageResourceId)
+
+ override fun getTitle() = context.getString(R.string.tools_installer_title)
+
+ override fun onAttached() {
+ super.onAttached()
+ lifecycleScope.launch {
+ try {
+ val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
+ when {
+ state == ToolsInstaller.ERROR -> setState(State.INITIAL)
+ state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
+ state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
+ state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
+ else -> setState(State.INITIAL)
+ }
+ } catch (_: Throwable) {
+ setState(State.INITIAL)
+ }
+ }
+ }
+
+ override fun onClick() {
+ setState(State.WORKING)
+ lifecycleScope.launch {
+ try {
+ val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
+ when {
+ result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
+ result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
+ else -> setState(State.FAILURE)
+ }
+ } catch (_: Throwable) {
+ setState(State.FAILURE)
+ }
+ }
+ }
+
+ private fun setState(state: State) {
+ if (this.state == state) return
+ this.state = state
+ if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
+ notifyChanged()
+ }
+
+ private enum class State(val messageResourceId: Int, val shouldEnableView: Boolean) {
+ INITIAL(R.string.tools_installer_initial, true),
+ ALREADY(R.string.tools_installer_already, false),
+ FAILURE(R.string.tools_installer_failure, true),
+ WORKING(R.string.tools_installer_working, false),
+ INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
+ SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
+ INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
+ SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
new file mode 100644
index 00000000..7d997e8a
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.wireguard.android.Application
+import com.wireguard.android.BuildConfig
+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
+import kotlinx.coroutines.withContext
+
+class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ private var versionSummary: String? = null
+
+ override fun getSummary() = versionSummary
+
+ override fun getTitle() = context.getString(R.string.version_title, BuildConfig.VERSION_NAME)
+
+ override fun onClick() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.data = Uri.parse("https://www.wireguard.com/")
+ try {
+ context.startActivity(intent)
+ } catch (e: Throwable) {
+ Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ companion object {
+ private fun getBackendPrettyName(context: Context, backend: Backend) = when (backend) {
+ is WgQuickBackend -> context.getString(R.string.type_name_kernel_module)
+ is GoBackend -> context.getString(R.string.type_name_go_userspace)
+ else -> ""
+ }
+ }
+
+ init {
+ lifecycleScope.launch {
+ val backend = Application.getBackend()
+ versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).lowercase())
+ notifyChanged()
+ versionSummary = try {
+ getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
+ } catch (_: Throwable) {
+ getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).lowercase())
+ }
+ notifyChanged()
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
new file mode 100644
index 00000000..220796e0
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.util.AttributeSet
+import android.util.Log
+import androidx.preference.Preference
+import com.google.android.material.snackbar.Snackbar
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.util.AdminKnobs
+import com.wireguard.android.util.BiometricAuthenticator
+import com.wireguard.android.util.DownloadsFileSaver
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.activity
+import com.wireguard.android.util.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.nio.charset.StandardCharsets
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+/**
+ * Preference implementing a button that asynchronously exports config zips.
+ */
+class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
+ private var exportedFilePath: String? = null
+ private val downloadsFileSaver = DownloadsFileSaver(activity)
+
+ private fun exportZip() {
+ lifecycleScope.launch {
+ val tunnels = Application.getTunnelManager().getTunnels()
+ try {
+ exportedFilePath = withContext(Dispatchers.IO) {
+ val configs = tunnels.map { async(SupervisorJob()) { it.getConfigAsync() } }.awaitAll()
+ if (configs.isEmpty()) {
+ throw IllegalArgumentException(context.getString(R.string.no_tunnels_error))
+ }
+ val outputFile = downloadsFileSaver.save("wireguard-export.zip", "application/zip", true)
+ if (outputFile == null) {
+ withContext(Dispatchers.Main.immediate) {
+ isEnabled = true
+ }
+ return@withContext null
+ }
+ try {
+ ZipOutputStream(outputFile.outputStream).use { zip ->
+ for (i in configs.indices) {
+ zip.putNextEntry(ZipEntry(tunnels[i].name + ".conf"))
+ zip.write(configs[i].toWgQuickString().toByteArray(StandardCharsets.UTF_8))
+ }
+ zip.closeEntry()
+ }
+ } catch (e: Throwable) {
+ outputFile.delete()
+ throw e
+ }
+ outputFile.fileName
+ }
+ notifyChanged()
+ } catch (e: Throwable) {
+ val error = ErrorMessages[e]
+ val message = context.getString(R.string.zip_export_error, error)
+ Log.e(TAG, message, e)
+ Snackbar.make(
+ activity.findViewById(android.R.id.content),
+ message, Snackbar.LENGTH_LONG
+ ).show()
+ isEnabled = true
+ }
+ }
+ }
+
+ 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)
+
+ override fun onClick() {
+ if (AdminKnobs.disableConfigExport) return
+ val fragment = activity.supportFragmentManager.fragments.first()
+ BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, fragment) {
+ when (it) {
+ // When we have successful authentication, or when there is no biometric hardware available.
+ is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
+ isEnabled = false
+ exportZip()
+ }
+
+ is BiometricAuthenticator.Result.Failure -> {
+ Snackbar.make(
+ activity.findViewById(android.R.id.content),
+ it.message,
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+
+ is BiometricAuthenticator.Result.Cancelled -> {}
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/ZipExporterPreference"
+ }
+}
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
new file mode 100644
index 00000000..2f90b2bb
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/AdminKnobs.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import android.content.RestrictionsManager
+import androidx.core.content.getSystemService
+import com.wireguard.android.Application
+
+object AdminKnobs {
+ private val restrictions: RestrictionsManager? = Application.get().getSystemService()
+ val disableConfigExport: Boolean
+ get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false)
+ ?: false
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
new file mode 100644
index 00000000..54d4da87
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.annotation.StringRes
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators
+import androidx.biometric.BiometricPrompt
+import androidx.fragment.app.Fragment
+import com.wireguard.android.R
+
+
+object BiometricAuthenticator {
+ private const val TAG = "WireGuard/BiometricAuthenticator"
+
+ // Not all devices support strong biometric auth so we're allowing both device credentials as
+ // well as weak biometrics.
+ private const val allowedAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
+
+ sealed class Result {
+ data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
+ data class Failure(val code: Int?, val message: CharSequence) : Result()
+ object HardwareUnavailableOrDisabled : Result()
+ object Cancelled : Result()
+ }
+
+ fun authenticate(
+ @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
+ }
+
+ else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
+ }
+ )
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ callback(Result.Failure(null, fragment.getString(R.string.biometric_auth_error)))
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ callback(Result.Success(result.cryptoObject))
+ }
+ }
+ val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(fragment.getString(dialogTitleRes))
+ .setAllowedAuthenticators(allowedAuthenticators)
+ .build()
+ if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
+ biometricPrompt.authenticate(promptInfo)
+ } else {
+ callback(Result.HardwareUnavailableOrDisabled)
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
new file mode 100644
index 00000000..c9a7f59d
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt
@@ -0,0 +1,37 @@
+/*
+ * 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
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.textfield.TextInputEditText
+import com.wireguard.android.R
+
+/**
+ * Standalone utilities for interacting with the system clipboard.
+ */
+object ClipboardUtils {
+ @JvmStatic
+ fun copyTextView(view: View) {
+ val data = when (view) {
+ is TextInputEditText -> Pair(view.editableText, view.hint)
+ is TextView -> Pair(view.text, view.contentDescription)
+ else -> return
+ }
+ if (data.first == null || data.first.isEmpty()) {
+ return
+ }
+ val service = view.context.getSystemService<ClipboardManager>() ?: return
+ service.setPrimaryClip(ClipData.newPlainText(data.second, data.first))
+ 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
new file mode 100644
index 00000000..ace1dc05
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/DownloadsFileSaver.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.util
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import android.provider.MediaStore.MediaColumns
+import androidx.activity.ComponentActivity
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import com.wireguard.android.R
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStream
+
+class DownloadsFileSaver(private val context: ComponentActivity) {
+ private lateinit var activityResult: ActivityResultLauncher<String>
+ private lateinit var futureGrant: CompletableDeferred<Boolean>
+
+ init {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ futureGrant = CompletableDeferred()
+ activityResult = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { ret -> futureGrant.complete(ret) }
+ }
+ }
+
+ suspend fun save(name: String, mimeType: String?, overwriteExisting: Boolean) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ withContext(Dispatchers.IO) {
+ val contentResolver = context.contentResolver
+ if (overwriteExisting)
+ contentResolver.delete(MediaStore.Downloads.EXTERNAL_CONTENT_URI, String.format("%s = ?", MediaColumns.DISPLAY_NAME), arrayOf(name))
+ val contentValues = ContentValues()
+ contentValues.put(MediaColumns.DISPLAY_NAME, name)
+ contentValues.put(MediaColumns.MIME_TYPE, mimeType)
+ val contentUri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
+ ?: throw IOException(context.getString(R.string.create_downloads_file_error))
+ val contentStream = contentResolver.openOutputStream(contentUri)
+ ?: throw IOException(context.getString(R.string.create_downloads_file_error))
+ @Suppress("DEPRECATION") var cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DATA), null, null, null)
+ var path: String? = null
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst())
+ path = cursor.getString(0)
+ } finally {
+ cursor.close()
+ }
+ }
+ if (path == null) {
+ path = "Download/"
+ cursor = contentResolver.query(contentUri, arrayOf(MediaColumns.DISPLAY_NAME), null, null, null)
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst())
+ path += cursor.getString(0)
+ } finally {
+ cursor.close()
+ }
+ }
+ }
+ DownloadsFile(context, contentStream, path, contentUri)
+ }
+ } else {
+ withContext(Dispatchers.Main.immediate) {
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ activityResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ val granted = futureGrant.await()
+ if (!granted) {
+ futureGrant = CompletableDeferred()
+ return@withContext null
+ }
+ }
+ @Suppress("DEPRECATION") val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ withContext(Dispatchers.IO) {
+ val file = File(path, name)
+ if (!path.isDirectory && !path.mkdirs())
+ throw IOException(context.getString(R.string.create_output_dir_error))
+ DownloadsFile(context, FileOutputStream(file), file.absolutePath, null)
+ }
+ }
+ }
+
+ class DownloadsFile(private val context: Context, val outputStream: OutputStream, val fileName: String, private val uri: Uri?) {
+ suspend fun delete() = withContext(Dispatchers.IO) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ context.contentResolver.delete(uri!!, null, null)
+ else
+ File(fileName).delete()
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
new file mode 100644
index 00000000..d617adec
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
@@ -0,0 +1,158 @@
+/*
+ * 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
+import com.wireguard.android.util.RootShell.RootShellException
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.InetEndpoint
+import com.wireguard.config.InetNetwork
+import com.wireguard.config.ParseException
+import com.wireguard.crypto.Key
+import com.wireguard.crypto.KeyFormatException
+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
+ )
+ 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
+ )
+ 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
+ )
+ private val KFE_TYPE_MAP = mapOf(
+ 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
+ )
+ 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
+ )
+
+ operator fun get(throwable: Throwable?): String {
+ val resources = Application.get().resources
+ if (throwable == null) return resources.getString(R.string.unknown_error)
+ val rootCause = rootCause(throwable)
+ return when {
+ rootCause is BadConfigException -> {
+ val reason = getBadConfigExceptionReason(resources, rootCause)
+ val context = if (rootCause.location == BadConfigException.Location.TOP_LEVEL) {
+ resources.getString(R.string.bad_config_context_top_level, rootCause.section.getName())
+ } else {
+ resources.getString(R.string.bad_config_context, rootCause.section.getName(), rootCause.location.getName())
+ }
+ 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 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)
+ }
+ }
+ }
+
+ 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!!.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) {
+ return resources.getString(R.string.bad_config_explanation_positive_number)
+ } else if (bce.location == BadConfigException.Location.PERSISTENT_KEEPALIVE) {
+ return resources.getString(R.string.bad_config_explanation_pka)
+ }
+ return ""
+ }
+
+ 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))
+ } else if (bce.cause is ParseException) {
+ val pe = bce.cause as ParseException?
+ val type = resources.getString((if (PE_CLASS_MAP.containsKey(pe!!.parsingClass)) PE_CLASS_MAP[pe.parsingClass] else R.string.parse_error_generic)!!)
+ return resources.getString(R.string.parse_error_reason, type, pe.text)
+ }
+ return resources.getString(BCE_REASON_MAP.getValue(bce.reason), bce.text)
+ }
+
+ private fun rootCause(throwable: Throwable): Throwable {
+ var cause = throwable
+ while (cause.cause != null) {
+ if (cause is BadConfigException || cause is BackendException ||
+ cause is RootShellException
+ ) break
+ val nextCause = cause.cause!!
+ if (nextCause is RemoteException) break
+ cause = nextCause
+ }
+ return cause
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/util/Extensions.kt b/ui/src/main/java/com/wireguard/android/util/Extensions.kt
new file mode 100644
index 00000000..3bc85051
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/Extensions.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import android.content.Context
+import android.util.TypedValue
+import androidx.annotation.AttrRes
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import com.wireguard.android.Application
+import com.wireguard.android.activity.SettingsActivity
+import kotlinx.coroutines.CoroutineScope
+
+fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
+ val typedValue = TypedValue()
+ theme.resolveAttribute(attrRes, typedValue, true)
+ return typedValue.data
+}
+
+val Any.applicationScope: CoroutineScope
+ get() = Application.getCoroutineScope()
+
+val Preference.activity: SettingsActivity
+ get() = context as? SettingsActivity
+ ?: throw IllegalStateException("Failed to resolve SettingsActivity")
+
+val Preference.lifecycleScope: CoroutineScope
+ get() = activity.lifecycleScope
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
new file mode 100644
index 00000000..f7de2465
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 {
+ val context = Application.get().applicationContext
+ return when {
+ bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
+ bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
+ bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
+ bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
+ else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..daefc378
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/TunnelImporter.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.net.Uri
+import android.provider.OpenableColumns
+import android.util.Log
+import androidx.fragment.app.FragmentManager
+import com.wireguard.android.Application
+import com.wireguard.android.R
+import com.wireguard.android.fragment.ConfigNamingDialogFragment
+import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.config.Config
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+object TunnelImporter {
+ suspend fun importTunnel(contentResolver: ContentResolver, uri: Uri, messageCallback: (CharSequence) -> Unit) = withContext(Dispatchers.IO) {
+ val context = Application.get().applicationContext
+ val futureTunnels = ArrayList<Deferred<ObservableTunnel>>()
+ val throwables = ArrayList<Throwable>()
+ try {
+ val columns = arrayOf(OpenableColumns.DISPLAY_NAME)
+ var name = ""
+ contentResolver.query(uri, columns, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst() && !cursor.isNull(0)) {
+ name = cursor.getString(0)
+ }
+ }
+ if (name.isEmpty()) {
+ name = Uri.decode(uri.lastPathSegment)
+ }
+ var idx = name.lastIndexOf('/')
+ if (idx >= 0) {
+ require(idx < name.length - 1) { context.getString(R.string.illegal_filename_error, name) }
+ name = name.substring(idx + 1)
+ }
+ val isZip = name.lowercase().endsWith(".zip")
+ if (name.lowercase().endsWith(".conf")) {
+ name = name.substring(0, name.length - ".conf".length)
+ } else {
+ require(isZip) { context.getString(R.string.bad_extension_error) }
+ }
+
+ if (isZip) {
+ ZipInputStream(contentResolver.openInputStream(uri)).use { zip ->
+ val reader = BufferedReader(InputStreamReader(zip, StandardCharsets.UTF_8))
+ var entry: ZipEntry?
+ while (true) {
+ entry = zip.nextEntry ?: break
+ name = entry.name
+ idx = name.lastIndexOf('/')
+ if (idx >= 0) {
+ if (idx >= name.length - 1) {
+ continue
+ }
+ name = name.substring(name.lastIndexOf('/') + 1)
+ }
+ if (name.lowercase().endsWith(".conf")) {
+ name = name.substring(0, name.length - ".conf".length)
+ } else {
+ continue
+ }
+ try {
+ Config.parse(reader)
+ } catch (e: Throwable) {
+ throwables.add(e)
+ null
+ }?.let {
+ val nameCopy = name
+ futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(nameCopy, it) })
+ }
+ }
+ }
+ } else {
+ futureTunnels.add(async(SupervisorJob()) { Application.getTunnelManager().create(name, Config.parse(contentResolver.openInputStream(uri)!!)) })
+ }
+
+ if (futureTunnels.isEmpty()) {
+ if (throwables.size == 1) {
+ throw throwables[0]
+ } else {
+ require(throwables.isNotEmpty()) { context.getString(R.string.no_configs_error) }
+ }
+ }
+ val tunnels = futureTunnels.mapNotNull {
+ try {
+ it.await()
+ } catch (e: Throwable) {
+ throwables.add(e)
+ null
+ }
+ }
+ withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(tunnels, throwables, messageCallback) }
+ } catch (e: Throwable) {
+ withContext(Dispatchers.Main.immediate) { onTunnelImportFinished(emptyList(), listOf(e), messageCallback) }
+ }
+ }
+
+ fun importTunnel(parentFragmentManager: FragmentManager, configText: String, messageCallback: (CharSequence) -> Unit) {
+ try {
+ // Ensure the config text is parseable before proceeding…
+ Config.parse(ByteArrayInputStream(configText.toByteArray(StandardCharsets.UTF_8)))
+
+ // Config text is valid, now create the tunnel…
+ ConfigNamingDialogFragment.newInstance(configText).show(parentFragmentManager, null)
+ } catch (e: Throwable) {
+ onTunnelImportFinished(emptyList(), listOf<Throwable>(e), messageCallback)
+ }
+ }
+
+ private fun onTunnelImportFinished(tunnels: List<ObservableTunnel>, throwables: Collection<Throwable>, messageCallback: (CharSequence) -> Unit) {
+ val context = Application.get().applicationContext
+ var message = ""
+ for (throwable in throwables) {
+ val error = ErrorMessages[throwable]
+ message = context.getString(R.string.import_error, error)
+ Log.e(TAG, message, throwable)
+ }
+ if (tunnels.size == 1 && throwables.isEmpty())
+ message = context.getString(R.string.import_success, tunnels[0].name)
+ else if (tunnels.isEmpty() && throwables.size == 1)
+ else if (throwables.isEmpty())
+ message = context.resources.getQuantityString(
+ R.plurals.import_total_success,
+ tunnels.size, tunnels.size
+ )
+ else if (!throwables.isEmpty())
+ message = context.resources.getQuantityString(
+ R.plurals.import_partial_success,
+ tunnels.size + throwables.size,
+ tunnels.size, tunnels.size + throwables.size
+ )
+
+ messageCallback(message)
+ }
+
+ private const val TAG = "WireGuard/TunnelImporter"
+} \ No newline at end of file
diff --git a/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
new file mode 100644
index 00000000..2c6ca8c3
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.core.stringSetPreferencesKey
+import com.wireguard.android.Application
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+object UserKnobs {
+ private val ENABLE_KERNEL_MODULE = booleanPreferencesKey("enable_kernel_module")
+ val enableKernelModule: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[ENABLE_KERNEL_MODULE] ?: false
+ }
+
+ suspend fun setEnableKernelModule(enable: Boolean?) {
+ Application.getPreferencesDataStore().edit {
+ if (enable == null)
+ it.remove(ENABLE_KERNEL_MODULE)
+ else
+ it[ENABLE_KERNEL_MODULE] = enable
+ }
+ }
+
+ private val MULTIPLE_TUNNELS = booleanPreferencesKey("multiple_tunnels")
+ val multipleTunnels: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[MULTIPLE_TUNNELS] ?: false
+ }
+
+ private val DARK_THEME = booleanPreferencesKey("dark_theme")
+ val darkTheme: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ 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 {
+ it[ALLOW_REMOTE_CONTROL_INTENTS] ?: false
+ }
+
+ private val RESTORE_ON_BOOT = booleanPreferencesKey("restore_on_boot")
+ val restoreOnBoot: Flow<Boolean>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[RESTORE_ON_BOOT] ?: false
+ }
+
+ private val LAST_USED_TUNNEL = stringPreferencesKey("last_used_tunnel")
+ val lastUsedTunnel: Flow<String?>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[LAST_USED_TUNNEL]
+ }
+
+ suspend fun setLastUsedTunnel(lastUsedTunnel: String?) {
+ Application.getPreferencesDataStore().edit {
+ if (lastUsedTunnel == null)
+ it.remove(LAST_USED_TUNNEL)
+ else
+ it[LAST_USED_TUNNEL] = lastUsedTunnel
+ }
+ }
+
+ private val RUNNING_TUNNELS = stringSetPreferencesKey("enabled_configs")
+ val runningTunnels: Flow<Set<String>>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[RUNNING_TUNNELS] ?: emptySet()
+ }
+
+ suspend fun setRunningTunnels(runningTunnels: Set<String>) {
+ Application.getPreferencesDataStore().edit {
+ if (runningTunnels.isEmpty())
+ it.remove(RUNNING_TUNNELS)
+ else
+ it[RUNNING_TUNNELS] = runningTunnels
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..c73b1efc
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt
@@ -0,0 +1,86 @@
+/*
+ * 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
+
+class ConfigProxy : Parcelable {
+ val `interface`: InterfaceProxy
+ val peers: ObservableList<PeerProxy> = ObservableArrayList()
+
+ private constructor(parcel: Parcel) {
+ `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) }
+ }
+
+ constructor(other: Config) {
+ `interface` = InterfaceProxy(other.getInterface())
+ other.peers.forEach {
+ val proxy = PeerProxy(it)
+ peers.add(proxy)
+ proxy.bind(this)
+ }
+ }
+
+ constructor() {
+ `interface` = InterfaceProxy()
+ }
+
+ fun addPeer(): PeerProxy {
+ val proxy = PeerProxy()
+ peers.add(proxy)
+ proxy.bind(this)
+ return proxy
+ }
+
+ override fun describeContents() = 0
+
+ @Throws(BadConfigException::class)
+ fun resolve(): Config {
+ val resolvedPeers: MutableCollection<Peer> = ArrayList()
+ peers.forEach { resolvedPeers.add(it.resolve()) }
+ return Config.Builder()
+ .setInterface(`interface`.resolve())
+ .addPeers(resolvedPeers)
+ .build()
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeParcelable(`interface`, flags)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ dest.writeParcelableList(peers, flags)
+ } else {
+ dest.writeTypedList(peers)
+ }
+ }
+
+ private class ConfigProxyCreator : Parcelable.Creator<ConfigProxy> {
+ override fun createFromParcel(parcel: Parcel): ConfigProxy {
+ return ConfigProxy(parcel)
+ }
+
+ override fun newArray(size: Int): Array<ConfigProxy?> {
+ return arrayOfNulls(size)
+ }
+ }
+
+ companion object {
+ @JvmField
+ val CREATOR: Parcelable.Creator<ConfigProxy> = ConfigProxyCreator()
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
new file mode 100644
index 00000000..004ebed1
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.viewmodel
+
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import androidx.databinding.ObservableArrayList
+import androidx.databinding.ObservableList
+import com.wireguard.android.BR
+import com.wireguard.config.Attribute
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Interface
+import com.wireguard.crypto.Key
+import com.wireguard.crypto.KeyFormatException
+import com.wireguard.crypto.KeyPair
+
+class InterfaceProxy : BaseObservable, Parcelable {
+ @get:Bindable
+ val excludedApplications: ObservableList<String> = ObservableArrayList()
+
+ @get:Bindable
+ val includedApplications: ObservableList<String> = ObservableArrayList()
+
+ @get:Bindable
+ var addresses: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.addresses)
+ }
+
+ @get:Bindable
+ var dnsServers: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.dnsServers)
+ }
+
+ @get:Bindable
+ var listenPort: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.listenPort)
+ }
+
+ @get:Bindable
+ var mtu: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.mtu)
+ }
+
+ @get:Bindable
+ var privateKey: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.privateKey)
+ notifyPropertyChanged(BR.publicKey)
+ }
+
+ @get:Bindable
+ val publicKey: String
+ get() = try {
+ KeyPair(Key.fromBase64(privateKey)).publicKey.toBase64()
+ } catch (ignored: KeyFormatException) {
+ ""
+ }
+
+ private constructor(parcel: Parcel) {
+ addresses = parcel.readString() ?: ""
+ dnsServers = parcel.readString() ?: ""
+ parcel.readStringList(excludedApplications)
+ parcel.readStringList(includedApplications)
+ listenPort = parcel.readString() ?: ""
+ mtu = parcel.readString() ?: ""
+ privateKey = parcel.readString() ?: ""
+ }
+
+ constructor(other: Interface) {
+ addresses = Attribute.join(other.addresses)
+ val dnsServerStrings = other.dnsServers.map { it.hostAddress }.plus(other.dnsSearchDomains)
+ dnsServers = Attribute.join(dnsServerStrings)
+ excludedApplications.addAll(other.excludedApplications)
+ includedApplications.addAll(other.includedApplications)
+ listenPort = other.listenPort.map { it.toString() }.orElse("")
+ mtu = other.mtu.map { it.toString() }.orElse("")
+ val keyPair = other.keyPair
+ privateKey = keyPair.privateKey.toBase64()
+ }
+
+ constructor()
+
+ override fun describeContents() = 0
+
+ fun generateKeyPair() {
+ val keyPair = KeyPair()
+ privateKey = keyPair.privateKey.toBase64()
+ notifyPropertyChanged(BR.privateKey)
+ notifyPropertyChanged(BR.publicKey)
+ }
+
+ @Throws(BadConfigException::class)
+ fun resolve(): Interface {
+ val builder = Interface.Builder()
+ if (addresses.isNotEmpty()) builder.parseAddresses(addresses)
+ if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers)
+ if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications)
+ if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications)
+ if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
+ if (mtu.isNotEmpty()) builder.parseMtu(mtu)
+ if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
+ return builder.build()
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeString(addresses)
+ dest.writeString(dnsServers)
+ dest.writeStringList(excludedApplications)
+ dest.writeStringList(includedApplications)
+ dest.writeString(listenPort)
+ dest.writeString(mtu)
+ dest.writeString(privateKey)
+ }
+
+ private class InterfaceProxyCreator : Parcelable.Creator<InterfaceProxy> {
+ override fun createFromParcel(parcel: Parcel): InterfaceProxy {
+ return InterfaceProxy(parcel)
+ }
+
+ override fun newArray(size: Int): Array<InterfaceProxy?> {
+ return arrayOfNulls(size)
+ }
+ }
+
+ companion object {
+ @JvmField
+ val CREATOR: Parcelable.Creator<InterfaceProxy> = InterfaceProxyCreator()
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
new file mode 100644
index 00000000..e78d0826
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerProxy.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.viewmodel
+
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import androidx.databinding.Observable
+import androidx.databinding.Observable.OnPropertyChangedCallback
+import androidx.databinding.ObservableList
+import com.wireguard.android.BR
+import com.wireguard.config.Attribute
+import com.wireguard.config.BadConfigException
+import com.wireguard.config.Peer
+import java.lang.ref.WeakReference
+
+class PeerProxy : BaseObservable, Parcelable {
+ private val dnsRoutes: MutableList<String?> = ArrayList()
+ private var allowedIpsState = AllowedIpsState.INVALID
+ private var interfaceDnsListener: InterfaceDnsListener? = null
+ private var peerListListener: PeerListListener? = null
+ private var owner: ConfigProxy? = null
+ private var totalPeers = 0
+
+ @get:Bindable
+ var allowedIps: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.allowedIps)
+ calculateAllowedIpsState()
+ }
+
+ @get:Bindable
+ var endpoint: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.endpoint)
+ }
+
+ @get:Bindable
+ var persistentKeepalive: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.persistentKeepalive)
+ }
+
+ @get:Bindable
+ var preSharedKey: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.preSharedKey)
+ }
+
+ @get:Bindable
+ var publicKey: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.publicKey)
+ }
+
+ @get:Bindable
+ val isAbleToExcludePrivateIps: Boolean
+ get() = allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS || allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD
+
+ @get:Bindable
+ val isExcludingPrivateIps: Boolean
+ get() = allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
+
+ private constructor(parcel: Parcel) {
+ allowedIps = parcel.readString() ?: ""
+ endpoint = parcel.readString() ?: ""
+ persistentKeepalive = parcel.readString() ?: ""
+ preSharedKey = parcel.readString() ?: ""
+ publicKey = parcel.readString() ?: ""
+ }
+
+ constructor(other: Peer) {
+ allowedIps = Attribute.join(other.allowedIps)
+ endpoint = other.endpoint.map { it.toString() }.orElse("")
+ persistentKeepalive = other.persistentKeepalive.map { it.toString() }.orElse("")
+ preSharedKey = other.preSharedKey.map { it.toBase64() }.orElse("")
+ publicKey = other.publicKey.toBase64()
+ }
+
+ constructor()
+
+ fun bind(owner: ConfigProxy) {
+ val interfaze: InterfaceProxy = owner.`interface`
+ val peers = owner.peers
+ if (interfaceDnsListener == null) interfaceDnsListener = InterfaceDnsListener(this)
+ interfaze.addOnPropertyChangedCallback(interfaceDnsListener!!)
+ setInterfaceDns(interfaze.dnsServers)
+ if (peerListListener == null) peerListListener = PeerListListener(this)
+ peers.addOnListChangedCallback(peerListListener)
+ setTotalPeers(peers.size)
+ this.owner = owner
+ }
+
+ private fun calculateAllowedIpsState() {
+ val newState: AllowedIpsState
+ newState = if (totalPeers == 1) {
+ // String comparison works because we only care if allowedIps is a superset of one of
+ // the above sets of (valid) *networks*. We are not checking for a superset based on
+ // the individual addresses in each set.
+ val networkStrings: Collection<String> = getAllowedIpsSet()
+ // If allowedIps contains both the wildcard and the public networks, then private
+ // networks aren't excluded!
+ if (networkStrings.containsAll(IPV4_WILDCARD))
+ AllowedIpsState.CONTAINS_IPV4_WILDCARD
+ else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
+ AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
+ else
+ AllowedIpsState.OTHER
+ } else {
+ AllowedIpsState.INVALID
+ }
+ if (newState != allowedIpsState) {
+ allowedIpsState = newState
+ notifyPropertyChanged(BR.ableToExcludePrivateIps)
+ notifyPropertyChanged(BR.excludingPrivateIps)
+ }
+ }
+
+ override fun describeContents() = 0
+
+ private fun getAllowedIpsSet() = setOf(*Attribute.split(allowedIps))
+
+ // Replace the first instance of the wildcard with the public network list, or vice versa.
+ // DNS servers only need to handled specially when we're excluding private IPs.
+ fun setExcludingPrivateIps(excludingPrivateIps: Boolean) {
+ if (!isAbleToExcludePrivateIps || isExcludingPrivateIps == excludingPrivateIps) return
+ val oldNetworks = if (excludingPrivateIps) IPV4_WILDCARD else IPV4_PUBLIC_NETWORKS
+ val newNetworks = if (excludingPrivateIps) IPV4_PUBLIC_NETWORKS else IPV4_WILDCARD
+ val input: Collection<String> = getAllowedIpsSet()
+ val outputSize = input.size - oldNetworks.size + newNetworks.size
+ val output: MutableCollection<String?> = LinkedHashSet(outputSize)
+ var replaced = false
+ // Replace the first instance of the wildcard with the public network list, or vice versa.
+ for (network in input) {
+ if (oldNetworks.contains(network)) {
+ if (!replaced) {
+ for (replacement in newNetworks) if (!output.contains(replacement)) output.add(replacement)
+ replaced = true
+ }
+ } else if (!output.contains(network)) {
+ output.add(network)
+ }
+ }
+ // DNS servers only need to handled specially when we're excluding private IPs.
+ if (excludingPrivateIps) output.addAll(dnsRoutes) else output.removeAll(dnsRoutes)
+ allowedIps = Attribute.join(output)
+ allowedIpsState = if (excludingPrivateIps) AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS else AllowedIpsState.CONTAINS_IPV4_WILDCARD
+ notifyPropertyChanged(BR.allowedIps)
+ notifyPropertyChanged(BR.excludingPrivateIps)
+ }
+
+ @Throws(BadConfigException::class)
+ fun resolve(): Peer {
+ val builder = Peer.Builder()
+ if (allowedIps.isNotEmpty()) builder.parseAllowedIPs(allowedIps)
+ if (endpoint.isNotEmpty()) builder.parseEndpoint(endpoint)
+ if (persistentKeepalive.isNotEmpty()) builder.parsePersistentKeepalive(persistentKeepalive)
+ if (preSharedKey.isNotEmpty()) builder.parsePreSharedKey(preSharedKey)
+ if (publicKey.isNotEmpty()) builder.parsePublicKey(publicKey)
+ return builder.build()
+ }
+
+ private fun setInterfaceDns(dnsServers: CharSequence) {
+ val newDnsRoutes = Attribute.split(dnsServers).filter { !it.contains(":") }.map { "$it/32" }
+ if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
+ val input = getAllowedIpsSet()
+ // Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
+ val output = input.filter { !dnsRoutes.contains(it) || newDnsRoutes.contains(it) }.plus(newDnsRoutes).distinct()
+ // None of the public networks are /32s, so this cannot change the AllowedIPs state.
+ allowedIps = Attribute.join(output)
+ notifyPropertyChanged(BR.allowedIps)
+ }
+ dnsRoutes.clear()
+ dnsRoutes.addAll(newDnsRoutes)
+ }
+
+ private fun setTotalPeers(totalPeers: Int) {
+ if (this.totalPeers == totalPeers) return
+ this.totalPeers = totalPeers
+ calculateAllowedIpsState()
+ }
+
+ fun unbind() {
+ if (owner == null) return
+ val interfaze: InterfaceProxy = owner!!.`interface`
+ val peers = owner!!.peers
+ if (interfaceDnsListener != null) interfaze.removeOnPropertyChangedCallback(interfaceDnsListener!!)
+ if (peerListListener != null) peers.removeOnListChangedCallback(peerListListener)
+ peers.remove(this)
+ setInterfaceDns("")
+ setTotalPeers(0)
+ owner = null
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeString(allowedIps)
+ dest.writeString(endpoint)
+ dest.writeString(persistentKeepalive)
+ dest.writeString(preSharedKey)
+ dest.writeString(publicKey)
+ }
+
+ private enum class AllowedIpsState {
+ CONTAINS_IPV4_PUBLIC_NETWORKS, CONTAINS_IPV4_WILDCARD, INVALID, OTHER
+ }
+
+ private class InterfaceDnsListener constructor(peerProxy: PeerProxy) : OnPropertyChangedCallback() {
+ private val weakPeerProxy: WeakReference<PeerProxy> = WeakReference(peerProxy)
+ override fun onPropertyChanged(sender: Observable, propertyId: Int) {
+ val peerProxy = weakPeerProxy.get()
+ if (peerProxy == null) {
+ sender.removeOnPropertyChangedCallback(this)
+ return
+ }
+ // This shouldn't be possible, but try to avoid a ClassCastException anyway.
+ if (sender !is InterfaceProxy) return
+ if (!(propertyId == BR._all || propertyId == BR.dnsServers)) return
+ peerProxy.setInterfaceDns(sender.dnsServers)
+ }
+ }
+
+ private class PeerListListener(peerProxy: PeerProxy) : ObservableList.OnListChangedCallback<ObservableList<PeerProxy?>>() {
+ private val weakPeerProxy: WeakReference<PeerProxy> = WeakReference(peerProxy)
+ override fun onChanged(sender: ObservableList<PeerProxy?>) {
+ val peerProxy = weakPeerProxy.get()
+ if (peerProxy == null) {
+ sender.removeOnListChangedCallback(this)
+ return
+ }
+ peerProxy.setTotalPeers(sender.size)
+ }
+
+ override fun onItemRangeChanged(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
+ // Do nothing.
+ }
+
+ override fun onItemRangeInserted(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
+ onChanged(sender)
+ }
+
+ override fun onItemRangeMoved(
+ sender: ObservableList<PeerProxy?>,
+ fromPosition: Int, toPosition: Int,
+ itemCount: Int
+ ) {
+ // Do nothing.
+ }
+
+ override fun onItemRangeRemoved(
+ sender: ObservableList<PeerProxy?>,
+ positionStart: Int, itemCount: Int
+ ) {
+ onChanged(sender)
+ }
+ }
+
+ private class PeerProxyCreator : Parcelable.Creator<PeerProxy> {
+ override fun createFromParcel(parcel: Parcel): PeerProxy {
+ return PeerProxy(parcel)
+ }
+
+ override fun newArray(size: Int): Array<PeerProxy?> {
+ return arrayOfNulls(size)
+ }
+ }
+
+ companion object {
+ @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"
+ )
+ 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
new file mode 100644
index 00000000..8c822dcb
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.widget
+
+import android.text.InputFilter
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+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? {
+ var replacement: SpannableStringBuilder? = null
+ var rIndex = 0
+ val dLength = dest.length
+ for (sIndex in sStart until sEnd) {
+ val c = source[sIndex]
+ val dIndex = dStart + (sIndex - sStart)
+ // 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
+ ) {
+ ++rIndex
+ } else {
+ if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
+ replacement.delete(rIndex, rIndex + 1)
+ }
+ }
+ return replacement
+ }
+
+ companion object {
+ private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || c == '+' || c == '/'
+
+ @JvmStatic
+ fun newInstance() = KeyInputFilter()
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
new file mode 100644
index 00000000..91c7da0c
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 android.widget.RelativeLayout
+import com.wireguard.android.R
+
+class MultiselectableRelativeLayout @JvmOverloads constructor(
+ context: Context? = null,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
+ private var multiselected = false
+
+ override fun onCreateDrawableState(extraSpace: Int): IntArray {
+ if (multiselected) {
+ val drawableState = super.onCreateDrawableState(extraSpace + 1)
+ View.mergeDrawableStates(drawableState, STATE_MULTISELECTED)
+ return drawableState
+ }
+ return super.onCreateDrawableState(extraSpace)
+ }
+
+ fun setMultiSelected(on: Boolean) {
+ if (!multiselected) {
+ multiselected = true
+ refreshDrawableState()
+ }
+ isActivated = on
+ }
+
+ fun setSingleSelected(on: Boolean) {
+ if (multiselected) {
+ multiselected = false
+ refreshDrawableState()
+ }
+ isActivated = on
+ }
+
+ companion object {
+ private val STATE_MULTISELECTED = intArrayOf(R.attr.state_multiselected)
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
new file mode 100644
index 00000000..e21ebaba
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.widget
+
+import android.text.InputFilter
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+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? {
+ var replacement: SpannableStringBuilder? = null
+ var rIndex = 0
+ val dLength = dest.length
+ for (sIndex in sStart until sEnd) {
+ val c = source[sIndex]
+ val dIndex = dStart + (sIndex - sStart)
+ // 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
+ ) {
+ ++rIndex
+ } else {
+ if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
+ replacement.delete(rIndex, rIndex + 1)
+ }
+ }
+ return replacement
+ }
+
+ companion object {
+ private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0
+
+ @JvmStatic
+ fun newInstance() = NameInputFilter()
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
new file mode 100644
index 00000000..0e2eeff1
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright © 2018 The Android Open Source Project
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.widget
+
+import android.animation.ObjectAnimator
+import android.content.res.ColorStateList
+import android.graphics.*
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.FloatProperty
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.N)
+class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
+ private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val mPath = Path()
+ private val mSlashRect = RectF()
+ private var mAnimationEnabled = true
+
+ // Animate this value on change
+ private var mCurrentSlashLength = 0f
+ private var mRotation = 0f
+ private var mSlashed = false
+
+ override fun draw(canvas: Canvas) {
+ canvas.save()
+ val m = Matrix()
+ val width = bounds.width()
+ val height = bounds.height()
+ 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)
+ )
+ mPath.reset()
+ // Draw the slash vertically
+ mPath.addRoundRect(mSlashRect, radiusX, radiusY, Path.Direction.CW)
+ // Rotate -45 + desired rotation
+ m.setRotate(mRotation + DEFAULT_ROTATION, width / 2f, height / 2f)
+ mPath.transform(m)
+ canvas.drawPath(mPath, mPaint)
+
+ // Rotate back to vertical
+ m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2f, height / 2f)
+ mPath.transform(m)
+
+ // Draw another rect right next to the first, for clipping
+ m.setTranslate(mSlashRect.width(), 0f)
+ mPath.transform(m)
+ mPath.addRoundRect(mSlashRect, 1f * width, 1f * height, Path.Direction.CW)
+ m.setRotate(mRotation + DEFAULT_ROTATION, width / 2f, height / 2f)
+ mPath.transform(m)
+ @Suppress("DEPRECATION")
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
+ canvas.clipPath(mPath, Region.Op.DIFFERENCE) else canvas.clipOutPath(mPath)
+ mDrawable.draw(canvas)
+ canvas.restore()
+ }
+
+ override fun getIntrinsicHeight() = mDrawable.intrinsicHeight
+
+ override fun getIntrinsicWidth() = mDrawable.intrinsicWidth
+
+ override fun getOpacity() = PixelFormat.OPAQUE
+
+ override fun onBoundsChange(bounds: Rect) {
+ super.onBoundsChange(bounds)
+ mDrawable.bounds = bounds
+ }
+
+ private fun scale(frac: Float, width: Int) = frac * width
+
+ override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) {
+ mDrawable.alpha = alpha
+ mPaint.alpha = alpha
+ }
+
+ fun setAnimationEnabled(enabled: Boolean) {
+ mAnimationEnabled = enabled
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ mDrawable.colorFilter = colorFilter
+ mPaint.colorFilter = colorFilter
+ }
+
+ private fun setDrawableTintList(tint: ColorStateList?) {
+ mDrawable.setTintList(tint)
+ }
+
+ fun setRotation(rotation: Float) {
+ if (mRotation == rotation) return
+ mRotation = rotation
+ invalidateSelf()
+ }
+
+ fun setSlashed(slashed: Boolean) {
+ if (mSlashed == slashed) return
+ mSlashed = slashed
+ val end = if (mSlashed) SLASH_HEIGHT / SCALE else 0f
+ val start = if (mSlashed) 0f else SLASH_HEIGHT / SCALE
+ if (mAnimationEnabled) {
+ val anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end)
+ anim.addUpdateListener { _ -> invalidateSelf() }
+ anim.duration = QS_ANIM_LENGTH
+ anim.start()
+ } else {
+ mCurrentSlashLength = end
+ invalidateSelf()
+ }
+ }
+
+ override fun setTint(@ColorInt tintColor: Int) {
+ super.setTint(tintColor)
+ mDrawable.setTint(tintColor)
+ mPaint.color = tintColor
+ }
+
+ override fun setTintList(tint: ColorStateList?) {
+ super.setTintList(tint)
+ setDrawableTintList(tint)
+ mPaint.color = tint?.defaultColor ?: 0
+ invalidateSelf()
+ }
+
+ override fun setTintMode(tintMode: PorterDuff.Mode?) {
+ super.setTintMode(tintMode)
+ mDrawable.setTintMode(tintMode)
+ }
+
+ private fun updateRect(left: Float, top: Float, right: Float, bottom: Float) {
+ mSlashRect.left = left
+ mSlashRect.top = top
+ mSlashRect.right = right
+ mSlashRect.bottom = bottom
+ }
+
+ companion object {
+ private const val CENTER_X = 10.65f
+ private const val CENTER_Y = 11.869239f
+ private val CORNER_RADIUS = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) 0f else 1f
+
+ // Draw the slash washington-monument style; rotate to no-u-turn style
+ private const val DEFAULT_ROTATION = -45f
+ private const val QS_ANIM_LENGTH: Long = 350
+ private const val SCALE = 24f
+ private const val SLASH_HEIGHT = 28f
+
+ // These values are derived in un-rotated (vertical) orientation
+ private const val SLASH_WIDTH = 1.8384776f
+
+ // Bottom is derived during animation
+ private const val LEFT = (CENTER_X - SLASH_WIDTH / 2) / SCALE
+ private const val RIGHT = (CENTER_X + SLASH_WIDTH / 2) / SCALE
+ private const val TOP = (CENTER_Y - SLASH_HEIGHT / 2) / SCALE
+ private val mSlashLengthProp: FloatProperty<SlashDrawable> = object : FloatProperty<SlashDrawable>("slashLength") {
+ override fun get(obj: SlashDrawable): Float {
+ return obj.mCurrentSlashLength
+ }
+
+ override fun setValue(obj: SlashDrawable, value: Float) {
+ obj.mCurrentSlashLength = value
+ }
+ }
+ }
+
+}
diff --git a/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt b/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt
new file mode 100644
index 00000000..0bde810d
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2013 The Android Open Source Project
+ * 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.os.Parcelable
+import android.util.AttributeSet
+import com.google.android.material.materialswitch.MaterialSwitch
+
+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) {
+ isRestoringState = true
+ super.onRestoreInstanceState(state)
+ isRestoringState = false
+ }
+
+ override fun setChecked(checked: Boolean) {
+ if (checked == isChecked) return
+ if (isRestoringState || listener == null) {
+ super.setChecked(checked)
+ return
+ }
+ isEnabled = false
+ listener!!.onBeforeCheckedChanged(this, checked)
+ }
+
+ fun setCheckedInternal(checked: Boolean) {
+ super.setChecked(checked)
+ isEnabled = true
+ }
+
+ fun setOnBeforeCheckedChangeListener(listener: OnBeforeCheckedChangeListener?) {
+ this.listener = listener
+ }
+
+ interface OnBeforeCheckedChangeListener {
+ fun onBeforeCheckedChanged(toggleSwitch: ToggleSwitch?, checked: Boolean)
+ }
+}
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/anim/scale_down.xml b/ui/src/main/res/anim/scale_down.xml
new file mode 100644
index 00000000..3d574180
--- /dev/null
+++ b/ui/src/main/res/anim/scale_down.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale
+ android:duration="300"
+ android:fromXScale="1.0"
+ android:fromYScale="1.0"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="0"
+ android:toYScale="0" />
+</set>
diff --git a/ui/src/main/res/anim/scale_up.xml b/ui/src/main/res/anim/scale_up.xml
new file mode 100644
index 00000000..e429b8bf
--- /dev/null
+++ b/ui/src/main/res/anim/scale_up.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <scale
+ android:duration="300"
+ android:fromXScale="0"
+ android:fromYScale="0"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="1.0"
+ android:toYScale="1.0" />
+</set>
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/app/src/main/res/drawable/ic_action_add_white.xml b/ui/src/main/res/drawable/ic_action_add_white.xml
index 0706462e..1ea440df 100644
--- a/app/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:viewportHeight="24.0"
- android:viewportWidth="24.0">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="#ffffff"
+ android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>
diff --git a/app/src/main/res/drawable/ic_action_delete.xml b/ui/src/main/res/drawable/ic_action_delete.xml
index 34f27019..73eb7352 100644
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="?android:attr/colorForeground"
+ 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/app/src/main/res/drawable/ic_action_edit.xml b/ui/src/main/res/drawable/ic_action_edit.xml
index 0a4d120e..d40c78d9 100644
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="?android:attr/colorForeground"
+ 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_generate.xml b/ui/src/main/res/drawable/ic_action_generate.xml
new file mode 100644
index 00000000..51d26aed
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_action_generate.xml
@@ -0,0 +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="#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/app/src/main/res/drawable/ic_action_open.xml b/ui/src/main/res/drawable/ic_action_open.xml
index ea56ba01..c9fd6fba 100644
--- a/app/src/main/res/drawable/ic_action_open.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:viewportHeight="24"
- android:viewportWidth="24">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="?android:attr/colorForeground"
+ 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/app/src/main/res/drawable/ic_action_save.xml b/ui/src/main/res/drawable/ic_action_save.xml
index 690d119d..6e618edb 100644
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="?android:attr/colorForeground"
+ 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/app/src/main/res/drawable/ic_action_scan_qr_code_white.xml b/ui/src/main/res/drawable/ic_action_scan_qr_code.xml
index cdd83361..4522ae46 100644
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="#ffffff"
+ 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> \ No newline at end of file
+</vector>
diff --git a/app/src/main/res/drawable/ic_action_select_all.xml b/ui/src/main/res/drawable/ic_action_select_all.xml
index 9c560297..490ca715 100644
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:fillColor="?android:attr/colorForeground"
+ 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
new file mode 100644
index 00000000..04ee5b74
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_action_share_white.xml
@@ -0,0 +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="#FFFFFFFF"
+ android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
+</vector>
diff --git a/ui/src/main/res/drawable/ic_arrow_back.xml b/ui/src/main/res/drawable/ic_arrow_back.xml
new file mode 100644
index 00000000..0df5dc6c
--- /dev/null
+++ b/ui/src/main/res/drawable/ic_arrow_back.xml
@@ -0,0 +1,15 @@
+<!--
+ ~ 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="#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/app/src/main/res/drawable/ic_launcher_foreground.xml b/ui/src/main/res/drawable/ic_launcher_foreground.xml
index f9713f37..a3dfe681 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/ui/src/main/res/drawable/ic_launcher_foreground.xml
@@ -2,8 +2,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
- android:viewportHeight="2160"
- android:viewportWidth="2160">
+ android:viewportWidth="2160"
+ android:viewportHeight="2160">
<group
android:scaleX="1"
android:scaleY="-1"
@@ -32,4 +32,4 @@
</group>
</group>
</group>
-</vector> \ No newline at end of file
+</vector>
diff --git a/app/src/main/res/drawable/ic_settings.xml b/ui/src/main/res/drawable/ic_settings.xml
index aabfce2a..af9f2634 100644
--- a/app/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:viewportHeight="24"
- android:viewportWidth="24">
+ 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/app/src/main/res/drawable/ic_tile.xml b/ui/src/main/res/drawable/ic_tile.xml
index eaf784c1..6232123f 100644
--- a/app/src/main/res/drawable/ic_tile.xml
+++ b/ui/src/main/res/drawable/ic_tile.xml
@@ -1,24 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="400dp"
- android:height="400dp"
- android:viewportHeight="400.0"
- android:viewportWidth="400.0">
+ android:width="200dp"
+ android:height="200dp"
+ android:viewportWidth="400.0"
+ android:viewportHeight="400.0">
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m197.7,0c-6.2,0.1 -12.5,0.5 -19,1.1 12.4,2.9 23.5,5.5 34.7,8.1 -0.1,0.7 -0.3,1.4 -0.4,2 -14.9,2 -29.1,-3.5 -43.7,-5.5 5.3,3.1 10.7,6 16.2,8.5 5.6,2.5 11.4,4.7 17.2,7 -7.4,6.3 -14.8,7.7 -24,5.6 -5.1,-1.2 -10.4,-1.8 -15.6,-1.5 -5.3,0.3 -10.7,1.6 -15.6,4.8 5.2,2.6 10,4.8 14.5,7.5 1.9,1.1 4,2.9 4.5,4.9 1.2,4.6 1.6,9.4 2.3,14.1 -8.5,1 -23.5,9.6 -26.5,15.3 13.1,2.5 27.3,-0.5 39.8,7.9 -4.1,3.1 -13.7,7 -17.2,9.6 4.3,1.1 14.4,0.6 18.3,0.3 3.3,-0.2 4.8,-0.3 6.2,0.8l38.5,30.1c4,3.3 20.4,18.7 24.7,28.5 3.6,8.3 4.1,15.3 4.1,17.1 -0,4.6 -0.6,11.8 -3.7,19.9 -1.3,3.4 -5.2,10.9 -13.3,19.6 -12.5,13.5 -28.6,20.9 -46.2,24.5 -40.9,8.4 -74.9,52.1 -65.3,100.2 11.2,56.2 73.2,86.6 123.9,59.9 32.8,-17.3 50.1,-51 45.5,-87.7 -2.8,-22.2 -12.8,-40.2 -29.7,-54.8 -2.2,-1.9 -3.6,-1.9 -6.2,-0.3 -8.7,5.6 -17.7,10.9 -26.8,15.9 -5.2,2.9 -10.9,5 -17.4,7.9 2.2,0.6 3.3,0.9 4.4,1.1 24.4,6.5 37.5,28 31.7,52 -5.1,21.4 -26.8,35 -47.9,31.4 -17.5,-3 -32.8,-17.6 -35.4,-35 -2.8,-19 6.7,-37.3 23.5,-44.9 9.3,-4.2 18.9,-7.9 28.3,-12.2 10.6,-4.9 22.1,-8.7 31.3,-15.5 23,-16.9 37.1,-40.1 42.7,-68.1 3.3,-16.8 3.1,-33.5 -4.6,-49.4 -5.9,-12.2 -15.6,-21.1 -26,-29.2 -10.7,-8.3 -22,-15.9 -32.7,-24.3 -2.9,-2.3 -4.8,-6.2 -6.2,-9.7 -0.6,-1.5 1.3,-5.6 2.5,-5.8 6.5,-1.2 13.2,-1.8 19.8,-2 7.7,-0.3 15.4,-0 23,0.1 1.7,0 3.9,-0.2 4.9,0.7 4,3.9 7.1,1.4 9.9,-1.2 2.3,-2.2 4,-5 5.8,-7.5 -1.1,-0.2 -3.4,-0.7 -5.7,-0.8 -7.7,-0.2 -15.4,-0.1 -23.2,-0.3 -1.4,-0.1 -2.7,-1.5 -4,-2.2 1.4,-0.6 2.8,-1.6 4.3,-1.6 13.3,-0.1 26.6,-0.1 40,-0.1 0,-6.9 -9.2,-16.4 -17.5,-19 -0.1,0.9 -0.1,1.8 -0.2,2.7 -8.2,0.2 -16.2,0 -23.5,-3.8 -1.9,-1 -3.2,-3.3 -4.7,-5 -2,-2.1 -3.6,-4.9 -6,-6.3 -4.9,-2.9 -10.3,-4.9 -15.4,-7.4C224.3,1.7 211.3,-0.1 197.7,0ZM249.6,29.4c0.6,-0 1.2,0.2 1.9,0.8 1,0.8 2,1.7 3.3,2.8 -1.5,0.8 -2.8,1.5 -4.1,2.2 -1.8,0.9 -3.1,0.3 -4.2,-1.1 -0.9,-1.2 -1,-2.3 0.3,-3.3 0.9,-0.7 1.8,-1.3 2.9,-1.3z"
- android:strokeColor="#00000000"
- android:strokeWidth="1.33333325" />
+ android:strokeWidth="1.33333325"
+ android:strokeColor="#00000000" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m97.9,307.6c-7.8,2 -15.4,4.9 -23.4,7.5 3.9,-26.3 34.7,-50.6 60.8,-47.8 -8.1,10.9 -11.8,23.3 -12.7,35.6 -8.7,1.6 -16.8,2.7 -24.8,4.7"
- android:strokeColor="#00000000"
- android:strokeWidth="1.33333325" />
+ android:strokeWidth="1.33333325"
+ android:strokeColor="#00000000" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="m134.3,124c41.3,-25.3 94,-9.8 113.8,28.2 3.7,7.2 4.2,18.3 1.8,25.8 -8.2,26.1 -27.5,40.7 -54.1,46.9 7.8,-6.7 14.1,-14.3 16,-24.8 2,-10.6 -0.1,-20.1 -6.2,-28.8 -9.3,-13.2 -27.3,-18.6 -42.4,-12.9 -16.3,6.2 -25.3,21.1 -23.7,39.4 1.5,17 14.4,28.1 38.6,32.2 -3.6,1.9 -6.4,3.3 -9.1,4.8 -11.1,6.2 -20.7,14.3 -28.2,24.6 -2.5,3.3 -4.1,3.6 -7.9,1.3 -48.6,-29.7 -51.7,-104.3 1.4,-136.8"
- android:strokeColor="#00000000"
- android:strokeWidth="1.33333325" />
+ android:strokeWidth="1.33333325"
+ android:strokeColor="#00000000" />
</vector>
diff --git a/app/src/main/res/drawable/list_item_background.xml b/ui/src/main/res/drawable/list_item_background.xml
index f86d4df1..16714e7b 100644
--- a/app/src/main/res/drawable/list_item_background.xml
+++ b/ui/src/main/res/drawable/list_item_background.xml
@@ -6,11 +6,9 @@
<item
android:state_activated="true"
app:state_multiselected="true">
- <color android:color="?attr/colorControlActivated" />
+ <color android:color="?attr/colorSurfaceVariant" />
</item>
- <item
- android:state_activated="true"
- app:state_multiselected="false">
+ <item android:state_activated="true">
<color android:color="?attr/colorControlHighlight" />
</item>
</selector>
diff --git a/ui/src/main/res/drawable/tv_logo_banner.xml b/ui/src/main/res/drawable/tv_logo_banner.xml
new file mode 100644
index 00000000..734702f3
--- /dev/null
+++ b/ui/src/main/res/drawable/tv_logo_banner.xml
@@ -0,0 +1,211 @@
+<!--
+ ~ 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="1934.5dp"
+ android:height="393.14dp"
+ android:viewportWidth="1934.5"
+ android:viewportHeight="393.14">
+ <path
+ android:fillColor="#88171a"
+ android:fillType="nonZero"
+ android:pathData="M0,0L1934.5,0L1934.5,393.14L0,393.14Z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="M432.69,266.47 L367.47,66.48l24.46,0l49.64,154.34 50.91,-154.34l22.28,0l50.72,153.98 49.81,-153.98L639.03,66.48L573.99,266.47L555.87,266.47L503.16,105.43 450.99,266.47Z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m634.56,111.4l24.64,0l0,155.07l-24.64,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m716.31,198.9l0,67.57L692.04,266.47L692.04,112.13l106.52,0c15.82,0 28.2,3.8 37.14,11.41 8.94,7.61 13.41,18.18 13.41,31.7 0.18,10.5 -3.6,20.69 -10.6,28.53 -7.06,8.03 -16.27,12.95 -27.63,14.77l42.93,67.93l-26.27,0l-43.84,-67.57zM716.31,176.25l82.24,0c8.33,0 14.7,-1.81 19.11,-5.43 4.41,-3.63 6.61,-8.82 6.61,-15.58 0,-6.77 -2.2,-11.93 -6.61,-15.49 -4.41,-3.56 -10.78,-5.34 -19.11,-5.34l-82.24,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="M871.98,266.47L871.98,111.77l145.1,0l0,22.64l-120.83,0l0,38.59l79.34,0l0,22.28l-79.34,0l0,48.91l127.53,0l0,22.28L871.97,266.47Z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m1194.46,212.31l0,-29.35l-54.53,0l0,-23.01l79.34,0l0,60.14c-9.66,16.67 -22.25,29.44 -37.77,38.31 -15.52,8.88 -33.13,13.32 -52.81,13.32 -29.95,0 -54.68,-9.87 -74.18,-29.62 -19.51,-19.75 -29.26,-44.78 -29.26,-75.09 0,-30.43 9.78,-55.52 29.34,-75.27 19.57,-19.75 44.27,-29.62 74.09,-29.62 18.48,0 35.32,4.05 50.54,12.13 15.2,8.08 28.01,20.01 37.13,34.6L1195.55,123.72c-6.13,-11.8 -15.58,-21.56 -27.17,-28.08 -12.09,-6.83 -25.79,-10.33 -39.67,-10.15 -22.46,0 -41.06,7.7 -55.8,23.1 -14.73,15.4 -22.1,34.87 -22.1,58.42 0,23.55 7.37,42.99 22.1,58.33 14.73,15.34 33.33,23.01 55.8,23.01 14.01,0 26.48,-3.02 37.41,-9.06 10.93,-6.04 20.38,-15.03 28.35,-26.99z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m1246.36,112.14l24.28,0l0,99.09c0,14.85 3.5,24.87 10.5,30.07 7.01,5.2 20.89,7.79 41.67,7.79 20.89,0 34.84,-2.59 41.84,-7.79 7.01,-5.19 10.51,-15.21 10.51,-30.07L1375.16,112.14l24.09,0l0,105.43c0,18.96 -6.01,32.67 -18.02,41.13 -12.02,8.45 -31.61,12.68 -58.79,12.68 -27.05,0 -46.49,-4.17 -58.33,-12.5 -11.84,-8.33 -17.75,-22.1 -17.75,-41.3z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m1395.46,266.47 l78.62,-154.7l15.22,0l79.34,154.7l-25.9,0l-20.11,-39.31l-81.7,0l-19.93,39.31zM1451.44,206.69l60.51,0l-30.07,-59.23z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m1595.56,198.9l0,67.57l-24.28,0L1571.29,112.13l106.52,0c15.82,0 28.2,3.8 37.14,11.41 8.94,7.61 13.41,18.18 13.41,31.7 0.18,10.5 -3.6,20.69 -10.6,28.53 -7.06,8.03 -16.27,12.95 -27.62,14.77l42.93,67.93l-26.27,0l-43.84,-67.57zM1595.56,176.25l82.24,0c8.33,0 14.7,-1.81 19.11,-5.43 4.41,-3.63 6.61,-8.82 6.61,-15.58 0,-6.77 -2.2,-11.93 -6.61,-15.49 -4.41,-3.56 -10.78,-5.34 -19.11,-5.34l-82.24,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m1820.26,111.77c25.24,0 45.59,7.21 61.05,21.65 15.46,14.43 23.19,33.18 23.19,56.25 0,23.31 -7.58,41.94 -22.73,55.89 -15.16,13.95 -35.66,20.92 -61.5,20.92l-68.66,0L1751.6,111.78l68.66,0zM1820.63,134.05l-44.75,0l0,110.14l44.75,0c18.35,0 32.79,-4.92 43.29,-14.77 10.51,-9.84 15.76,-23.21 15.76,-40.12 0,-16.3 -5.43,-29.59 -16.3,-39.85 -10.87,-10.26 -25.12,-15.4 -42.75,-15.4l0,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m429.67,318.63l0,18.61l-6.35,0l0,-40.45l37.37,0l0,5.92l-31.02,0l0,10.08l20.04,0l0,5.83z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m455.24,337.24 l20.56,-40.45l3.98,0l20.75,40.45l-6.78,0l-5.26,-10.28l-21.36,0l-5.21,10.28zM469.87,321.61l15.82,0l-7.86,-15.49z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m505.02,332.08 l3.08,-5.3c2.81,1.87 5.87,3.34 9.09,4.35 3.04,1.01 6.22,1.53 9.43,1.57 3.06,0.14 6.09,-0.62 8.71,-2.2 2.03,-1.16 3.3,-3.32 3.32,-5.66 0.08,-2.16 -1.1,-4.17 -3.03,-5.14 -2.03,-1.12 -5.21,-1.75 -9.57,-1.87 -7.36,-0.09 -12.41,-0.95 -15.16,-2.56 -2.75,-1.61 -4.13,-4.28 -4.13,-8.01 -0.04,-3.39 1.67,-6.56 4.53,-8.38 3.01,-2.12 7.06,-3.18 12.15,-3.18 3.43,-0.01 6.83,0.49 10.11,1.47 3.33,1.01 6.52,2.48 9.45,4.36l-3.17,5.26c-2.52,-1.7 -5.25,-3.03 -8.15,-3.95 -2.69,-0.9 -5.5,-1.37 -8.34,-1.4 -2.56,-0.11 -5.1,0.44 -7.39,1.61 -1.66,0.76 -2.75,2.39 -2.8,4.21 -0.03,1.77 1.13,3.33 2.82,3.84 1.88,0.76 5.14,1.14 9.78,1.14 6.44,0 11.31,1.09 14.59,3.27 3.29,2.18 4.93,5.42 4.93,9.71 0.01,3.8 -1.86,7.37 -5,9.52 -3.33,2.53 -7.65,3.79 -12.95,3.79 -3.95,-0.01 -7.88,-0.57 -11.67,-1.68 -3.75,-1.07 -7.34,-2.67 -10.63,-4.76z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m568.06,337.24l0,-34.81l-17.05,0l0,-5.63l40.45,0l0,5.63L574.36,302.43l0,34.81z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m604.52,345.48c0.91,-1.32 1.73,-2.7 2.46,-4.12 0.68,-1.32 1.25,-2.7 1.7,-4.12l-0.38,0c-0.96,0.01 -1.86,-0.44 -2.44,-1.21 -0.67,-0.87 -1.01,-1.95 -0.97,-3.06 -0.05,-1.22 0.36,-2.41 1.16,-3.34 0.73,-0.84 1.8,-1.32 2.92,-1.3 1.3,-0.02 2.52,0.64 3.22,1.73 0.89,1.39 1.32,3.02 1.23,4.67 -0.09,2.03 -0.71,3.99 -1.8,5.71 -1.5,2.45 -3.3,4.7 -5.35,6.7z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m668.13,337.24l0,-40.45l5.25,0l16.58,23.68 16.58,-23.68l5.26,0l0,40.45l-6.3,0l0,-29.08l-15.54,22.49 -15.49,-22.49l0,29.08z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m748.83,338.52c-7.2,0 -13.12,-2.04 -17.76,-6.11 -4.42,-3.87 -6.96,-9.46 -6.96,-15.33 -0.01,-5.87 2.52,-11.47 6.94,-15.34 4.63,-4.02 10.55,-6.04 17.78,-6.04 7.3,0 13.25,2.01 17.88,6.02 4.42,3.89 6.95,9.48 6.94,15.36 -0.01,5.88 -2.54,11.47 -6.96,15.35 -4.64,4.06 -10.59,6.09 -17.85,6.09zM748.83,332.6c5.4,0 9.81,-1.46 13.22,-4.38 3.25,-2.79 5.12,-6.86 5.11,-11.15 -0.01,-4.29 -1.89,-8.35 -5.14,-11.14 -3.43,-2.94 -7.82,-4.4 -13.19,-4.4 -5.36,0 -9.75,1.46 -13.14,4.38 -3.23,2.8 -5.09,6.87 -5.09,11.15 0,4.28 1.86,8.35 5.09,11.16 3.39,2.92 7.78,4.38 13.14,4.38z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m803.92,296.79c6.6,0 11.92,1.89 15.96,5.66 4.04,3.78 6.06,8.68 6.06,14.71 0,6.09 -1.98,10.96 -5.94,14.61 -3.96,3.65 -9.32,5.47 -16.08,5.47l-17.95,0l0,-40.45zM804.02,302.61l-11.7,0l0,28.8l11.7,0c4.8,0 8.57,-1.29 11.32,-3.86 2.75,-2.57 4.13,-6.07 4.13,-10.49 0.13,-3.93 -1.42,-7.72 -4.27,-10.42 -2.84,-2.68 -6.57,-4.03 -11.18,-4.03z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m837.87,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m894.94,319.57l0,17.67l-6.35,0L888.59,296.88l27.85,0c4.14,0 7.37,0.99 9.71,2.98 2.38,2.08 3.67,5.14 3.51,8.29 0.05,2.75 -0.94,5.41 -2.77,7.46 -1.86,2.11 -4.43,3.48 -7.22,3.86l11.22,17.76l-6.87,0l-11.46,-17.67zM894.94,313.65l21.5,0c1.78,0.12 3.55,-0.39 5,-1.42 1.18,-1.01 1.82,-2.52 1.72,-4.07 0.1,-1.55 -0.54,-3.05 -1.72,-4.05 -1.46,-1.02 -3.22,-1.52 -5,-1.4l-21.5,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m943.11,337.24l0,-40.45l3.32,0l25.72,28.42l0,-28.42l6.2,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m999,345.48c0.91,-1.32 1.73,-2.7 2.46,-4.12 0.68,-1.32 1.25,-2.7 1.7,-4.12l-0.37,0c-0.96,0.01 -1.87,-0.44 -2.44,-1.21 -0.67,-0.87 -1.01,-1.95 -0.97,-3.06 -0.05,-1.22 0.37,-2.41 1.16,-3.34 0.72,-0.84 1.8,-1.32 2.91,-1.3 1.3,-0.02 2.52,0.64 3.22,1.73 0.89,1.39 1.32,3.02 1.23,4.67 -0.09,2.03 -0.71,3.99 -1.8,5.71 -1.5,2.45 -3.3,4.7 -5.35,6.7z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1059.16,332.08 l3.08,-5.3c2.81,1.87 5.87,3.34 9.09,4.35 3.04,1.01 6.22,1.53 9.43,1.57 3.06,0.14 6.09,-0.62 8.71,-2.2 2.03,-1.16 3.3,-3.32 3.31,-5.66 0.09,-2.16 -1.1,-4.17 -3.03,-5.14 -2.02,-1.12 -5.21,-1.75 -9.57,-1.87 -7.35,-0.09 -12.41,-0.95 -15.15,-2.56 -2.75,-1.61 -4.12,-4.28 -4.12,-8.01 -0.04,-3.39 1.67,-6.56 4.53,-8.38 3.02,-2.12 7.06,-3.18 12.15,-3.18 3.43,-0.01 6.83,0.49 10.11,1.47 3.33,1.01 6.52,2.48 9.45,4.36l-3.17,5.26c-2.51,-1.7 -5.25,-3.03 -8.15,-3.95 -2.69,-0.9 -5.51,-1.37 -8.34,-1.4 -2.56,-0.11 -5.1,0.44 -7.39,1.61 -1.66,0.76 -2.74,2.39 -2.8,4.21 -0.03,1.77 1.13,3.33 2.82,3.84 1.88,0.76 5.14,1.14 9.78,1.14 6.44,0 11.3,1.09 14.59,3.27 3.28,2.18 4.93,5.41 4.93,9.71 0.01,3.8 -1.86,7.37 -5,9.52 -3.33,2.53 -7.65,3.79 -12.95,3.79 -3.95,-0.01 -7.88,-0.57 -11.68,-1.68 -3.75,-1.07 -7.33,-2.67 -10.63,-4.76z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1112.56,337.24l0,-40.45l37.94,0l0,5.92L1118.91,302.71l0,10.09l20.74,0l0,5.82L1118.91,318.63l0,12.79l33.34,0l0,5.82z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1206.06,327.34c-2.21,3.55 -5.35,6.42 -9.07,8.31 -3.96,1.96 -8.34,2.94 -12.77,2.87 -7.1,0 -12.95,-2.01 -17.55,-6.04 -4.38,-3.89 -6.89,-9.48 -6.89,-15.34 0,-5.87 2.51,-11.45 6.89,-15.35 4.59,-4.06 10.44,-6.08 17.55,-6.08 4.15,-0.06 8.25,0.79 12.03,2.49 3.47,1.55 6.47,3.97 8.71,7.04l-5.02,3.51c-1.7,-2.33 -3.97,-4.16 -6.6,-5.33 -2.87,-1.28 -5.98,-1.92 -9.12,-1.87 -4.69,-0.17 -9.27,1.41 -12.86,4.43 -3.23,2.78 -5.09,6.84 -5.09,11.11 0.01,4.27 1.87,8.32 5.11,11.1 3.58,3.02 8.16,4.59 12.83,4.43 3.3,0.07 6.57,-0.66 9.52,-2.13 2.93,-1.58 5.41,-3.86 7.24,-6.64z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1218.66,296.89l6.34,0l0,25.91c0,3.89 0.92,6.51 2.75,7.86 1.83,1.36 5.46,2.04 10.89,2.04 5.46,0 9.11,-0.68 10.94,-2.04 1.83,-1.36 2.75,-3.98 2.75,-7.86l0,-25.91l6.3,0l0,27.56c0,4.96 -1.57,8.54 -4.71,10.75 -3.14,2.21 -8.27,3.31 -15.37,3.31 -7.07,0 -12.16,-1.09 -15.25,-3.27 -3.09,-2.18 -4.64,-5.78 -4.64,-10.8z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1280.86,319.57l0,17.67l-6.34,0L1274.52,296.88l27.85,0c4.14,0 7.37,0.99 9.71,2.98 2.38,2.08 3.67,5.14 3.51,8.29 0.05,2.75 -0.94,5.41 -2.77,7.46 -1.86,2.11 -4.43,3.48 -7.22,3.86l11.22,17.76l-6.86,0l-11.46,-17.67zM1280.86,313.65l21.5,0c1.78,0.12 3.55,-0.39 5,-1.42 1.18,-1.01 1.82,-2.52 1.73,-4.07 0.1,-1.55 -0.55,-3.05 -1.73,-4.05 -1.46,-1.02 -3.22,-1.52 -5,-1.4l-21.5,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1328.96,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1431.46,337.24l-3.27,0l-20.32,-40.45l6.72,0l15.2,30.27 14.97,-30.27l6.96,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1468.06,319.57l0,17.67l-6.35,0L1461.71,296.79l29.41,0c4.17,0 7.42,0.99 9.73,2.96 2.37,2.09 3.66,5.15 3.48,8.31 0.1,3.13 -1.16,6.16 -3.46,8.29 -2.49,2.22 -5.76,3.37 -9.09,3.22zM1468.06,313.65l23.06,0c1.77,0.11 3.51,-0.39 4.95,-1.42 2.28,-2.21 2.31,-5.85 0.07,-8.1 -1.35,-1.03 -3.04,-1.53 -4.73,-1.42l-23.35,0z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1515.26,337.24l0,-40.45l3.32,0l25.71,28.42l0,-28.42l6.21,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1610.06,337.24l0,-34.81l-17.05,0l0,-5.63l40.45,0l0,5.63l-17.1,0l0,34.81z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1641.76,296.89l6.35,0l0,25.91c0,3.89 0.91,6.51 2.75,7.86 1.83,1.36 5.46,2.04 10.89,2.04 5.46,0 9.11,-0.68 10.94,-2.04 1.83,-1.36 2.75,-3.98 2.75,-7.86l0,-25.91l6.3,0l0,27.56c0,4.96 -1.57,8.54 -4.71,10.75 -3.14,2.21 -8.27,3.31 -15.37,3.31 -7.07,0 -12.16,-1.09 -15.25,-3.27 -3.09,-2.18 -4.64,-5.78 -4.64,-10.8z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1697.66,337.24l0,-40.45l3.31,0l25.72,28.42l0,-28.42l6.21,0l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1748.76,337.24l0,-40.45l3.32,0l25.71,28.42L1777.79,296.79L1784,296.79l0,40.45l-3.32,0l-25.67,-28.32l0,28.32z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1799.76,337.24l0,-40.45l37.94,0l0,5.92l-31.59,0l0,10.09l20.75,0l0,5.82l-20.75,0l0,12.79l33.34,0l0,5.82z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1850.46,337.24l0,-40.45l6.35,0l0,34.62l30.03,0l0,5.82z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="M329.74,185.56C329.74,185.56 336.68,40 176.7,40 35.22,40 30.8,179.63 30.8,179.63 30.8,179.63 9.99,340 179.96,340 342.98,340 329.74,185.56 329.74,185.56ZM131.94,134.7c30.02,-18.36 68.36,-7.14 82.73,20.47 2.72,5.23 3.07,13.29 1.34,18.78 -5.95,18.96 -20.02,29.59 -39.31,34.1 5.69,-4.87 10.22,-10.4 11.66,-18.03 1.52,-7.31 -0.13,-14.93 -4.55,-20.95 -7.03,-9.65 -19.59,-13.49 -30.81,-9.39 -11.89,4.51 -18.39,15.35 -17.22,28.68 1.09,12.38 10.48,20.41 28.06,23.45 -2.63,1.39 -4.65,2.42 -6.63,3.52 -8.05,4.41 -15.06,10.51 -20.54,17.87 -1.79,2.41 -3.01,2.6 -5.73,0.94 -35.34,-21.61 -37.61,-75.84 0.98,-99.45zM105.49,268.23c-5.68,1.44 -11.18,3.57 -16.98,5.47 2.84,-19.15 25.27,-36.79 44.23,-34.78 -5.49,7.57 -8.7,16.56 -9.24,25.9 -6.3,1.16 -12.25,1.94 -18.01,3.4zM226.28,81.25c5.61,0.21 11.23,0.12 16.84,0.25 1.4,0.09 2.8,0.29 4.17,0.58 -1.26,1.93 -2.67,3.75 -4.23,5.43 -2.01,1.87 -4.28,3.7 -7.17,0.85 -0.69,-0.68 -2.34,-0.53 -3.55,-0.54 -5.58,-0.07 -11.17,-0.25 -16.75,-0.04 -4.84,0.16 -9.66,0.65 -14.43,1.47 -0.9,0.16 -2.23,3.13 -1.82,4.22 0.97,2.59 2.38,5.44 4.47,7.09 7.75,6.11 15.97,11.59 23.75,17.66 7.56,5.9 14.59,12.36 18.87,21.25 5.58,11.59 5.74,23.74 3.34,35.95 -4.02,20.38 -14.33,37.26 -31.03,49.53 -6.73,4.94 -15.06,7.74 -22.77,11.29 -6.78,3.13 -13.75,5.81 -20.55,8.9 -12.25,5.57 -19.13,18.87 -17.1,32.69 1.85,12.69 12.98,23.27 25.73,25.46 15.29,2.62 31.07,-7.32 34.81,-22.86 4.2,-17.48 -5.29,-33.08 -23.07,-37.81 -0.78,-0.21 -1.57,-0.41 -3.2,-0.83 4.75,-2.12 8.86,-3.64 12.66,-5.72 6.61,-3.64 13.11,-7.49 19.48,-11.56 1.88,-1.2 2.89,-1.2 4.49,0.18 12.23,10.57 19.52,23.72 21.56,39.84 3.39,26.68 -9.25,51.2 -33.07,63.76 -36.87,19.44 -81.97,-2.68 -90.11,-43.55 -6.97,-35 17.73,-66.75 47.46,-72.88 12.79,-2.63 24.48,-7.96 33.57,-17.81 5.87,-6.35 8.71,-11.81 9.68,-14.27 1.81,-4.61 2.73,-9.52 2.72,-14.47 -0.2,-4.29 -1.2,-8.49 -2.96,-12.4 -3.1,-7.07 -15,-18.33 -17.94,-20.7l-28,-21.92c-0.98,-0.81 -2.1,-0.75 -4.51,-0.59 -2.86,0.19 -10.18,0.6 -13.33,-0.23 2.55,-1.93 9.52,-4.74 12.51,-7.01 -9.08,-6.13 -19.43,-3.92 -28.94,-5.75 2.2,-4.09 13.08,-10.39 19.27,-11.09 -0.37,-3.46 -0.93,-6.89 -1.69,-10.28 -0.38,-1.39 -1.93,-2.74 -3.29,-3.54 -3.29,-1.93 -6.77,-3.52 -10.55,-5.43 3.39,-2.19 7.31,-3.4 11.33,-3.51 3.82,-0.15 7.63,0.23 11.35,1.1 6.74,1.54 12.12,0.54 17.49,-4.05 -4.22,-1.7 -8.45,-3.25 -12.54,-5.09 -4.03,-1.84 -7.96,-3.9 -11.78,-6.16 10.62,1.48 20.89,5.46 31.75,4 0.09,-0.49 0.19,-0.99 0.28,-1.48 -8.12,-1.89 -16.24,-3.78 -25.23,-5.87 15.04,-1.37 29.04,-1.6 42.3,4.85 3.73,1.82 7.63,3.32 11.21,5.4 1.74,1.02 2.92,3.01 4.35,4.56 1.13,1.23 2.05,2.88 3.44,3.63 5.3,2.82 11.13,2.93 17.08,2.79 0.05,-0.68 0.09,-1.31 0.13,-1.99 5.98,1.87 12.72,8.77 12.71,13.8 -9.69,0 -19.37,-0.04 -29.06,0.06 -1.04,0.01 -2.06,0.77 -3.09,1.17 0.98,0.57 1.94,1.6 2.94,1.64z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#ffffff"
+ android:fillType="nonZero"
+ android:pathData="m213.78,66.91c-0.84,0.53 -0.94,1.71 -0.19,2.37 0.61,1.08 1.99,1.45 3.07,0.82 0.94,-0.47 1.85,-0.97 2.98,-1.57 -0.91,-0.78 -1.64,-1.42 -2.39,-2.03 -1.32,-1.09 -2.41,-0.41 -3.47,0.41z"
+ android:strokeColor="#00000000" />
+ <path
+ android:fillColor="#d1d1d1"
+ android:fillType="nonZero"
+ android:pathData="m1890,97.71l-1.24,0c-7.56,0.08 -11.84,6.64 -11.84,13 0,6.16 3.92,12.32 12.2,12.32l0.56,0c8.6,-0.24 12.36,-6.16 12.36,-12.24l0,-0.72c-0.28,-6.36 -4.84,-12.36 -12.04,-12.36zM1884.48,102.87l0,14.44l1.8,0l0,-12.96l3.4,0c1.88,0 2.8,1.16 2.8,2.32 0,1.4 -1.36,3 -3.8,3 -0.37,0 -0.84,-0.08 -1.28,-0.16l5.44,7.8l2.36,0l-4.92,-6.64c2.4,-0.44 4,-2.36 4,-4.24 0,-1.76 -1.52,-3.56 -4.88,-3.56zM1881.04,103.16c2.52,-2.72 5.16,-3.72 8.24,-3.72l1.28,0c5.48,0.24 9.44,5.8 9.44,11.16 0,0.76 -0.08,1.64 -0.24,2.36 -0.84,4.92 -5.12,8.36 -10.08,8.36 -0.12,0 -0.37,0.04 -0.49,0.04 -6.28,0 -10.28,-5.68 -10.28,-11.36 0,-0.48 0.08,-1.12 0.12,-1.64 0,0 0.28,-3.12 2,-5.2z"
+ android:strokeColor="#00000000" />
+</vector>
diff --git a/ui/src/main/res/layout-sw600dp/main_activity.xml b/ui/src/main/res/layout-sw600dp/main_activity.xml
new file mode 100644
index 00000000..b5ce34a4
--- /dev/null
+++ b/ui/src/main/res/layout-sw600dp/main_activity.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/main_activity_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".activity.MainActivity">
+
+ <LinearLayout
+ android:id="@+id/master_detail_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:divider="?attr/dividerHorizontal"
+ android:orientation="horizontal"
+ android:showDividers="middle">
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/list_fragment"
+ android:name="com.wireguard.android.fragment.TunnelListFragment"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:tag="LIST" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/detail_container"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3" />
+ </LinearLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
new file mode 100644
index 00000000..3a5ddf0a
--- /dev/null
+++ b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/bottom_sheet_top_padding">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/create_from_file"
+ 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"
+ android:layout_marginLeft="@dimen/normal_margin"
+ android:layout_marginEnd="@dimen/normal_margin"
+ android:layout_marginRight="@dimen/normal_margin"
+ android:nextFocusDown="@id/create_from_qrcode"
+ android:nextFocusForward="@id/create_from_qrcode"
+ android:text="@string/create_from_file"
+ android:textAlignment="viewStart"
+ android:textColor="?attr/colorOnSurface"
+ 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"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="parent"
+ app:rippleColor="?attr/colorSecondary" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/create_from_qrcode"
+ 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"
+ android:layout_marginLeft="@dimen/normal_margin"
+ android:layout_marginEnd="@dimen/normal_margin"
+ android:layout_marginRight="@dimen/normal_margin"
+ android:nextFocusUp="@id/create_from_file"
+ android:nextFocusDown="@id/create_empty"
+ android:nextFocusForward="@id/create_empty"
+ android:text="@string/create_from_qr_code"
+ android:textAlignment="viewStart"
+ android:textColor="?attr/colorOnSurface"
+ 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"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/create_from_file"
+ app:rippleColor="?attr/colorSecondary" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/create_empty"
+ 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"
+ android:layout_marginLeft="@dimen/normal_margin"
+ android:layout_marginEnd="@dimen/normal_margin"
+ android:layout_marginRight="@dimen/normal_margin"
+ android:nextFocusUp="@id/create_from_qrcode"
+ android:text="@string/create_empty"
+ android:textAlignment="viewStart"
+ android:textColor="?attr/colorOnSurface"
+ app:icon="@drawable/ic_action_edit"
+ app:iconPadding="@dimen/bottom_sheet_icon_padding"
+ app:iconTint="?attr/colorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/create_from_qrcode"
+ app:rippleColor="?attr/colorSecondary" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/ui/src/main/res/layout/app_list_dialog_fragment.xml b/ui/src/main/res/layout/app_list_dialog_fragment.xml
new file mode 100644
index 00000000..4000c64b
--- /dev/null
+++ b/ui/src/main/res/layout/app_list_dialog_fragment.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <data>
+
+ <import type="android.view.View" />
+
+ <import type="com.wireguard.android.model.ApplicationData" />
+
+ <variable
+ name="fragment"
+ type="com.wireguard.android.fragment.AppListDialogFragment" />
+
+ <variable
+ name="appData"
+ type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, ApplicationData&gt;" />
+ </data>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tabs"
+ style="@style/Widget.Material3.TabLayout.OnSurface"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.tabs.TabItem
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/exclude_from_tunnel" />
+
+ <com.google.android.material.tabs.TabItem
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/include_in_tunnel" />
+ </com.google.android.material.tabs.TabLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:minHeight="200dp">
+
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"
+ tools:visibility="gone" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/app_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:items="@{appData}"
+ app:layout="@{@layout/app_list_item}"
+ tools:itemCount="10"
+ tools:listitem="@layout/app_list_item" />
+ </FrameLayout>
+ </LinearLayout>
+</layout>
diff --git a/app/src/main/res/layout/app_list_item.xml b/ui/src/main/res/layout/app_list_item.xml
index 825b828b..d6a4b715 100644
--- a/app/src/main/res/layout/app_list_item.xml
+++ b/ui/src/main/res/layout/app_list_item.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
<data>
@@ -7,7 +8,7 @@
<variable
name="collection"
- type="com.wireguard.android.util.ObservableKeyedList&lt;String, com.wireguard.android.model.ApplicationData&gt;" />
+ type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, com.wireguard.android.model.ApplicationData&gt;" />
<variable
name="key"
@@ -23,33 +24,38 @@
android:layout_height="wrap_content"
android:background="@drawable/list_item_background"
android:gravity="center_vertical"
- android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}"
+ android:onClick="@{(view) -> item.setSelected(!item.selected)}"
android:orientation="horizontal"
- android:padding="16dp">
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
<ImageView
android:id="@+id/app_icon"
android:layout_width="32dp"
android:layout_height="32dp"
- android:src="@{item.icon}" />
+ android:layout_marginStart="16dp"
+ android:src="@{item.icon}"
+ tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/app_name"
- style="?android:attr/textAppearanceMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
- android:paddingEnd="8dp"
- android:paddingStart="8dp"
- android:text="@{key}" />
+ android:text="@{key}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ tools:text="@tools:sample/full_names" />
<CheckBox
- android:id="@+id/excluded_checkbox"
+ android:id="@+id/selected_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:checked="@={item.excludedFromTunnel}" />
+ android:checked="@={item.selected}"
+ tools:checked="true" />
</LinearLayout>
</layout>
diff --git a/app/src/main/res/layout/config_naming_dialog_fragment.xml b/ui/src/main/res/layout/config_naming_dialog_fragment.xml
index a7017804..63d3141d 100644
--- a/app/src/main/res/layout/config_naming_dialog_fragment.xml
+++ b/ui/src/main/res/layout/config_naming_dialog_fragment.xml
@@ -17,17 +17,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <EditText
+ <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tunnel_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/tunnel_name"
+ android:imeOptions="actionDone"
android:inputType="textNoSuggestions|textVisiblePassword"
- app:filter="@{NameInputFilter.newInstance()}" />
+ app:filter="@{NameInputFilter.newInstance()}">
+
+ <requestFocus />
+ </com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
-
</layout>
diff --git a/ui/src/main/res/layout/log_viewer_activity.xml b/ui/src/main/res/layout/log_viewer_activity.xml
new file mode 100644
index 00000000..2a377a15
--- /dev/null
+++ b/ui/src/main/res/layout/log_viewer_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:itemCount="20"
+ tools:listitem="@layout/log_viewer_entry" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/share_fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ app:srcCompat="@drawable/ic_action_share_white" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/ui/src/main/res/layout/log_viewer_entry.xml b/ui/src/main/res/layout/log_viewer_entry.xml
new file mode 100644
index 00000000..762d2ddf
--- /dev/null
+++ b/ui/src/main/res/layout/log_viewer_entry.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="6dp">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/log_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceBodySmall"
+ android:textColor="?android:attr/textColorPrimary"
+ 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"
+ 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" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/main_activity.xml b/ui/src/main/res/layout/main_activity.xml
index a4fd7fd1..a970d8e2 100644
--- a/app/src/main/res/layout/main_activity.xml
+++ b/ui/src/main/res/layout/main_activity.xml
@@ -1,19 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/master_detail_wrapper"
+ android:id="@+id/main_activity_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
- <fragment
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/detail_container"
android:name="com.wireguard.android.fragment.TunnelListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="LIST" />
-
- <FrameLayout
- android:id="@+id/detail_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-</FrameLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml
new file mode 100644
index 00000000..332df04a
--- /dev/null
+++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml
@@ -0,0 +1,321 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <data>
+
+ <import type="com.wireguard.android.backend.Tunnel.State" />
+
+ <import type="com.wireguard.android.util.ClipboardUtils" />
+
+ <variable
+ name="fragment"
+ type="com.wireguard.android.fragment.TunnelDetailFragment" />
+
+ <variable
+ name="tunnel"
+ type="com.wireguard.android.model.ObservableTunnel" />
+
+ <variable
+ name="config"
+ type="com.wireguard.config.Config" />
+ </data>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface"
+ android:clickable="true"
+ android:focusable="true">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/tunnel_detail_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="8dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/interface_title"
+ 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" />
+
+ <com.wireguard.android.widget.ToggleSwitch
+ android:id="@+id/tunnel_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:nextFocusDown="@id/interface_name_text"
+ android:nextFocusForward="@id/interface_name_text"
+ app:checked="@{tunnel.state == State.UP}"
+ app:layout_constraintBaseline_toBottomOf="@+id/interface_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:onBeforeCheckedChanged="@{fragment::setTunnelState}" />
+
+ <TextView
+ android:id="@+id/interface_name_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/interface_name_text"
+ android:text="@string/name"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/interface_title" />
+
+ <TextView
+ android:id="@+id/interface_name_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/name"
+ android:nextFocusUp="@id/tunnel_switch"
+ android:nextFocusDown="@id/public_key_text"
+ android:nextFocusForward="@id/public_key_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{tunnel.name}"
+ 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="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/public_key_text"
+ android:text="@string/public_key"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/interface_name_text" />
+
+ <TextView
+ android:id="@+id/public_key_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/public_key"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:nextFocusUp="@id/interface_name_text"
+ android:nextFocusDown="@id/addresses_text"
+ android:nextFocusForward="@id/addresses_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:singleLine="true"
+ android:text="@{config.interface.keyPair.publicKey.toBase64}"
+ 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="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/addresses_text"
+ android:text="@string/addresses"
+ android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/public_key_text" />
+
+ <TextView
+ android:id="@+id/addresses_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/addresses"
+ android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/dns_servers_text"
+ android:nextFocusForward="@id/dns_servers_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.interface.addresses}"
+ android: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"
+ tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
+
+ <TextView
+ android:id="@+id/dns_servers_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/dns_servers_text"
+ android:text="@string/dns_servers"
+ android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/addresses_text" />
+
+ <TextView
+ android:id="@+id/dns_servers_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/dns_servers"
+ android:nextFocusUp="@id/addresses_text"
+ android:nextFocusDown="@id/dns_search_domains_text"
+ android:nextFocusForward="@id/dns_search_domains_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.interface.dnsServers}"
+ android: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"
+ tools:text="8.8.8.8, 8.8.4.4" />
+
+ <TextView
+ android:id="@+id/dns_search_domains_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/dns_search_domain_text"
+ android:text="@string/dns_search_domains"
+ android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
+
+ <TextView
+ android:id="@+id/dns_search_domains_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/dns_search_domains"
+ android:nextFocusUp="@id/dns_servers_text"
+ android:nextFocusDown="@id/listen_port_text"
+ 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"
+ tools:text="zx2c4.com" />
+
+ <TextView
+ android:id="@+id/listen_port_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/listen_port_text"
+ android:text="@string/listen_port"
+ android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintEnd_toStartOf="@id/mtu_label"
+ app:layout_constraintHorizontal_weight="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/dns_search_domains_text" />
+
+ <TextView
+ android:id="@+id/listen_port_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/listen_port"
+ android:nextFocusRight="@id/mtu_text"
+ android:nextFocusUp="@id/dns_search_domains_text"
+ android:nextFocusDown="@id/applications_text"
+ 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"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/listen_port_label"
+ tools:text="51820" />
+
+ <TextView
+ android:id="@+id/mtu_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/mtu_text"
+ android:text="@string/mtu"
+ android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="0.5"
+ app:layout_constraintLeft_toRightOf="@id/listen_port_label"
+ app:layout_constraintStart_toEndOf="@id/listen_port_label"
+ app:layout_constraintTop_toBottomOf="@id/dns_search_domains_text" />
+
+ <TextView
+ android:id="@+id/mtu_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/mtu"
+ android:nextFocusLeft="@id/listen_port_text"
+ android:nextFocusUp="@id/dns_servers_text"
+ android:nextFocusForward="@id/applications_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.interface.mtu}"
+ android: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"
+ app:layout_constraintStart_toEndOf="@id/listen_port_label"
+ app:layout_constraintStart_toStartOf="@+id/mtu_label"
+ app:layout_constraintTop_toBottomOf="@+id/mtu_label"
+ tools:text="1500" />
+
+ <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="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_mtu_barrier" />
+
+ <TextView
+ android:id="@+id/applications_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/applications"
+ android:nextFocusUp="@id/mtu_text"
+ android:nextFocusDown="@id/peers_layout"
+ android:nextFocusForward="@id/peers_layout"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}"
+ android: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"
+ tools:text="8 excluded" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.google.android.material.card.MaterialCardView>
+
+ <LinearLayout
+ android:id="@+id/peers_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:divider="@null"
+ android:orientation="vertical"
+ app:items="@{config.peers}"
+ app:layout="@{@layout/tunnel_detail_peer}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/tunnel_detail_card"
+ tools:ignore="UselessLeaf" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+</layout> \ 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
new file mode 100644
index 00000000..25081cea
--- /dev/null
+++ b/ui/src/main/res/layout/tunnel_detail_peer.xml
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <data>
+
+ <import type="com.wireguard.android.util.ClipboardUtils" />
+
+ <variable
+ name="item"
+ type="com.wireguard.config.Peer" />
+ </data>
+
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/peer_title"
+ 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="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/public_key_text"
+ android:text="@string/public_key"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/peer_title" />
+
+ <TextView
+ android:id="@+id/public_key_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/public_key"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:nextFocusDown="@id/pre_shared_key_text"
+ android:nextFocusForward="@id/pre_shared_key_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:singleLine="true"
+ android:text="@{item.publicKey.toBase64}"
+ 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="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/pre_shared_key_text"
+ android:text="@string/pre_shared_key"
+ android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/public_key_text" />
+
+ <TextView
+ android:id="@+id/pre_shared_key_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/pre_shared_key"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/allowed_ips_text"
+ android:nextFocusForward="@id/allowed_ips_text"
+ android:singleLine="true"
+ android:text="@string/pre_shared_key_enabled"
+ android: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"
+ tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" />
+
+ <TextView
+ android:id="@+id/allowed_ips_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/allowed_ips_text"
+ android:text="@string/allowed_ips"
+ android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_text" />
+
+ <TextView
+ android:id="@+id/allowed_ips_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/allowed_ips"
+ android:nextFocusUp="@id/pre_shared_key_text"
+ android:nextFocusDown="@id/endpoint_text"
+ android:nextFocusForward="@id/endpoint_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{item.allowedIps}"
+ android: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"
+ tools:text="0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3" />
+
+ <TextView
+ android:id="@+id/endpoint_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/endpoint_text"
+ android:text="@string/endpoint"
+ android:visibility="@{!item.endpoint.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/allowed_ips_text" />
+
+ <TextView
+ android:id="@+id/endpoint_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/endpoint"
+ android:nextFocusUp="@id/allowed_ips_text"
+ android:nextFocusDown="@id/persistent_keepalive_text"
+ android:nextFocusForward="@id/persistent_keepalive_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{item.endpoint}"
+ android: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"
+ tools:text="192.168.0.1:51820" />
+
+ <TextView
+ android:id="@+id/persistent_keepalive_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/persistent_keepalive_text"
+ android:text="@string/persistent_keepalive"
+ android:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/endpoint_text" />
+
+ <TextView
+ android:id="@+id/persistent_keepalive_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/persistent_keepalive"
+ android:nextFocusUp="@id/endpoint_text"
+ android:nextFocusDown="@id/transfer_text"
+ android:nextFocusForward="@id/transfer_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{@plurals/persistent_keepalive_seconds_unit(item.persistentKeepalive.orElse(0), item.persistentKeepalive.orElse(0))}"
+ android: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"
+ tools:text="every 3 seconds" />
+
+ <TextView
+ android:id="@+id/transfer_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/persistent_keepalive_text"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/transfer_text"
+ android:text="@string/transfer"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/persistent_keepalive_text"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/transfer_text"
+ 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
new file mode 100644
index 00000000..0350486b
--- /dev/null
+++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml
@@ -0,0 +1,292 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <data>
+
+ <import type="com.wireguard.android.util.ClipboardUtils" />
+
+ <import type="com.wireguard.android.widget.KeyInputFilter" />
+
+ <import type="com.wireguard.android.widget.NameInputFilter" />
+
+ <variable
+ name="fragment"
+ type="com.wireguard.android.fragment.TunnelEditorFragment" />
+
+ <variable
+ name="config"
+ type="com.wireguard.android.viewmodel.ConfigProxy" />
+
+ <variable
+ name="name"
+ type="String" />
+ </data>
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:id="@+id/main_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorSurface">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="16dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/interface_title"
+ 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" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/interface_name_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/name"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/interface_title">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/interface_name_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusDown="@id/private_key_text"
+ android:nextFocusForward="@id/private_key_text"
+ android:text="@={name}"
+ app:filter="@{NameInputFilter.newInstance()}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/private_key_text_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/private_key"
+ app:endIconContentDescription="@string/generate_new_private_key"
+ app:endIconDrawable="@drawable/ic_action_generate"
+ app:endIconMode="custom"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/interface_name_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/private_key_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textPassword"
+ android:nextFocusUp="@id/interface_name_text"
+ android:nextFocusDown="@id/public_key_text"
+ android:nextFocusForward="@id/public_key_text"
+ android:onClick="@{fragment::onKeyClick}"
+ android:text="@={config.interface.privateKey}"
+ app:filter="@{KeyInputFilter.newInstance()}"
+ app:onFocusChange="@{fragment::onKeyFocusChange}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/public_key_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/public_key"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/private_key_text_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/public_key_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:editable="false"
+ android:ellipsize="end"
+ android:focusable="false"
+ android:hint="@string/hint_generated"
+ android:imeOptions="actionNext"
+ android:nextFocusUp="@id/private_key_text"
+ android:nextFocusDown="@id/addresses_label_text"
+ android:nextFocusForward="@id/addresses_label_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:singleLine="true"
+ android:text="@{config.interface.publicKey}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/addresses_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/addresses"
+ app:layout_constraintEnd_toStartOf="@id/listen_port_label_layout"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintHorizontal_weight="0.7"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/public_key_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/addresses_label_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/dns_servers_text"
+ android:nextFocusForward="@id/listen_port_text"
+ android:text="@={config.interface.addresses}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/listen_port_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/listen_port"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="0.3"
+ app:layout_constraintStart_toEndOf="@id/addresses_label_layout"
+ app:layout_constraintTop_toBottomOf="@id/public_key_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/listen_port_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/hint_random"
+ android:imeOptions="actionNext"
+ android:inputType="number"
+ android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/mtu_text"
+ android:nextFocusForward="@id/dns_servers_text"
+ android:text="@={config.interface.listenPort}"
+ android:textAlignment="center" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/dns_servers_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/dns_servers"
+ app:layout_constraintEnd_toStartOf="@id/mtu_label_layout"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintHorizontal_weight="0.7"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/addresses_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/dns_servers_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/addresses_label_text"
+ android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusForward="@id/mtu_text"
+ android:text="@={config.interface.dnsServers}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/mtu_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/mtu"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="0.3"
+ app:layout_constraintStart_toEndOf="@id/dns_servers_label_layout"
+ app:layout_constraintTop_toBottomOf="@id/addresses_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/mtu_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/hint_automatic"
+ android:imeOptions="actionDone"
+ android:inputType="number"
+ android:nextFocusUp="@id/listen_port_text"
+ android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusForward="@id/set_excluded_applications"
+ android:text="@={config.interface.mtu}"
+ android:textAlignment="center" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/set_excluded_applications"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:nextFocusUp="@id/dns_servers_text"
+ android:nextFocusDown="@id/peers_layout"
+ android:nextFocusForward="@id/peers_layout"
+ android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
+ android:text="@{config.interface.includedApplications.size > 0 ? @plurals/set_included_applications(config.interface.includedApplications.size, config.interface.includedApplications.size) : config.interface.excludedApplications.size > 0 ? @plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size) : @string/all_applications}"
+ android:textColor="?attr/colorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/mtu_label_layout"
+ app:rippleColor="?attr/colorSecondary"
+ tools:text="4 excluded applications" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.google.android.material.card.MaterialCardView>
+
+ <LinearLayout
+ android:id="@+id/peers_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:orientation="vertical"
+ app:fragment="@{fragment}"
+ app:items="@{config.peers}"
+ app:layout="@{@layout/tunnel_editor_peer}"
+ tools:ignore="UselessLeaf" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/add_peer_button"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:background="?attr/colorPrimaryDark"
+ android:gravity="center"
+ android:onClick="@{() -> config.addPeer()}"
+ android:text="@string/add_peer"
+ android:textColor="?attr/colorSecondary"
+ app:layout_anchorGravity="bottom"
+ app:rippleColor="?attr/colorSecondary" />
+ </LinearLayout>
+ </ScrollView>
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+</layout>
diff --git a/ui/src/main/res/layout/tunnel_editor_peer.xml b/ui/src/main/res/layout/tunnel_editor_peer.xml
new file mode 100644
index 00000000..305fa927
--- /dev/null
+++ b/ui/src/main/res/layout/tunnel_editor_peer.xml
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <data>
+
+ <import type="android.view.View" />
+
+ <import type="com.wireguard.android.widget.KeyInputFilter" />
+
+ <import type="com.wireguard.android.databinding.BindingAdapters" />
+
+ <variable
+ name="collection"
+ type="androidx.databinding.ObservableList&lt;com.wireguard.android.viewmodel.PeerProxy&gt;" />
+
+ <variable
+ name="item"
+ type="com.wireguard.android.viewmodel.PeerProxy" />
+
+ <variable
+ name="fragment"
+ type="com.wireguard.android.fragment.TunnelEditorFragment" />
+ </data>
+
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginBottom="4dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/peer_title"
+ 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" />
+
+ <androidx.appcompat.widget.AppCompatImageButton
+ android:id="@+id/delete"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="@null"
+ android:nextFocusDown="@id/public_key_text"
+ android:nextFocusForward="@id/public_key_text"
+ android:onClick="@{() -> item.unbind()}"
+ android:padding="8dp"
+ android:src="@drawable/ic_action_delete"
+ app:layout_constraintBaseline_toBaselineOf="@id/peer_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/peer_title" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/public_key_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/public_key"
+ app:layout_constraintBottom_toTopOf="@+id/pre_shared_key_label_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/peer_title">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/public_key_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/delete"
+ android:nextFocusDown="@id/pre_shared_key_text"
+ android:nextFocusForward="@id/pre_shared_key_text"
+ android:text="@={item.publicKey}"
+ app:filter="@{KeyInputFilter.newInstance()}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pre_shared_key_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/pre_shared_key"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/public_key_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pre_shared_key_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/hint_optional"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textPassword"
+ android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/persistent_keepalive_text"
+ android:nextFocusForward="@id/persistent_keepalive_text"
+ android:onClick="@{fragment::onKeyClick}"
+ android:text="@={item.preSharedKey}"
+ app:filter="@{KeyInputFilter.newInstance()}"
+ app:onFocusChange="@{fragment::onKeyFocusChange}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/persistent_keepalive_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/persistent_keepalive"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pre_shared_key_label_layout"
+ app:suffixText="@{@plurals/persistent_keepalive_seconds_suffix(BindingAdapters.tryParseInt(item.persistentKeepalive))}">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/persistent_keepalive_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/hint_optional_discouraged"
+ android:imeOptions="actionNext"
+ android:inputType="number"
+ android:nextFocusUp="@id/persistent_keepalive_text"
+ android:nextFocusDown="@id/endpoint_text"
+ android:nextFocusForward="@id/endpoint_text"
+ android:text="@={item.persistentKeepalive}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/endpoint_label_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/endpoint"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/persistent_keepalive_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/endpoint_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/persistent_keepalive_text"
+ android:nextFocusDown="@id/allowed_ips_text"
+ android:nextFocusForward="@id/allowed_ips_text"
+ android:text="@={item.endpoint}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/allowed_ips_label_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/allowed_ips"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/endpoint_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/allowed_ips_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:imeOptions="actionDone"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/endpoint_text"
+ android:nextFocusDown="@id/selected_checkbox"
+ android:nextFocusForward="@id/selected_checkbox"
+ android:text="@={item.allowedIps}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <CheckBox
+ android:id="@+id/selected_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginStart="4dp"
+ android:layout_marginTop="0dp"
+ android:checked="@={item.excludingPrivateIps}"
+ android:nextFocusDown="@id/add_peer_button"
+ android:nextFocusForward="@id/add_peer_button"
+ android:text="@string/exclude_private_ips"
+ android:visibility="@{item.ableToExcludePrivateIps ? View.VISIBLE : View.GONE}"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/allowed_ips_label_layout" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.google.android.material.card.MaterialCardView>
+</layout>
diff --git a/app/src/main/res/layout/tunnel_list_fragment.xml b/ui/src/main/res/layout/tunnel_list_fragment.xml
index 3a7c1c7a..2ee2ff38 100644
--- a/app/src/main/res/layout/tunnel_list_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_list_fragment.xml
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
<data>
- <import type="com.wireguard.android.model.Tunnel" />
+ <import type="com.wireguard.android.model.ObservableTunnel" />
<variable
name="fragment"
@@ -16,34 +17,38 @@
<variable
name="tunnels"
- type="com.wireguard.android.util.ObservableKeyedList&lt;String, Tunnel&gt;" />
+ type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, ObservableTunnel&gt;" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?android:attr/colorBackground"
+ android:background="?attr/colorSurface"
android:clipChildren="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tunnel_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:choiceMode="multipleChoiceModal"
android:clipToPadding="false"
+ android:nextFocusDown="@id/create_fab"
+ android:nextFocusForward="@id/create_fab"
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
app:configurationHandler="@{rowConfigurationHandler}"
app:items="@{tunnels}"
- app:layout="@{@layout/tunnel_list_item}" />
+ app:layout="@{@layout/tunnel_list_item}"
+ tools:itemCount="12"
+ tools:listitem="@layout/tunnel_list_item" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
- android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}">
+ android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
+ tools:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/logo_placeholder"
@@ -51,7 +56,6 @@
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginBottom="20dp"
- android:layout_marginTop="-70dp"
android:alpha="0.3333333"
android:src="@mipmap/ic_launcher" />
@@ -59,47 +63,20 @@
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>
- <com.wireguard.android.widget.fab.FloatingActionsMenu
- android:id="@+id/create_menu"
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/create_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
- android:clipChildren="false"
- app:fab_labelStyle="@style/fab_label"
- app:fab_labelsPosition="@integer/label_position"
- app:layout_behavior="com.wireguard.android.widget.fab.FloatingActionButtonBehavior">
+ android:nextFocusUp="@id/tunnel_list"
+ app:srcCompat="@drawable/ic_action_add_white" />
- <com.wireguard.android.widget.fab.LabeledFloatingActionButton
- android:id="@+id/create_from_file"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="@{fragment::onRequestImportConfig}"
- app:fabSize="mini"
- app:fab_title="@string/create_from_file"
- app:srcCompat="@drawable/ic_action_open_white" />
-
- <com.wireguard.android.widget.fab.LabeledFloatingActionButton
- android:id="@+id/create_from_qrcode"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="@{fragment::onRequestScanQRCode}"
- app:fabSize="mini"
- app:fab_title="@string/create_from_qr_code"
- app:srcCompat="@drawable/ic_action_scan_qr_code_white" />
-
- <com.wireguard.android.widget.fab.LabeledFloatingActionButton
- android:id="@+id/create_empty"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="@{fragment::onRequestCreateConfig}"
- app:fabSize="mini"
- app:fab_title="@string/create_empty"
- app:srcCompat="@drawable/ic_action_edit_white" />
- </com.wireguard.android.widget.fab.FloatingActionsMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
diff --git a/app/src/main/res/layout/tunnel_list_item.xml b/ui/src/main/res/layout/tunnel_list_item.xml
index 13e14fed..3d5b02d1 100644
--- a/app/src/main/res/layout/tunnel_list_item.xml
+++ b/ui/src/main/res/layout/tunnel_list_item.xml
@@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
<data>
- <import type="com.wireguard.android.model.Tunnel" />
+ <import type="com.wireguard.android.model.ObservableTunnel" />
- <import type="com.wireguard.android.model.Tunnel.State" />
+ <import type="com.wireguard.android.backend.Tunnel.State" />
<variable
name="collection"
- type="com.wireguard.android.util.ObservableKeyedList&lt;String, Tunnel&gt;" />
+ type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, ObservableTunnel&gt;" />
<variable
name="key"
@@ -18,7 +19,7 @@
<variable
name="item"
- type="com.wireguard.android.model.Tunnel" />
+ type="com.wireguard.android.model.ObservableTunnel" />
<variable
name="fragment"
@@ -33,27 +34,30 @@
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: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}" />
+ app:onBeforeCheckedChanged="@{fragment::setTunnelState}"
+ tools:checked="@sample/interface_names.json/names/checked/checked" />
</com.wireguard.android.widget.MultiselectableRelativeLayout>
</layout>
diff --git a/ui/src/main/res/layout/tv_activity.xml b/ui/src/main/res/layout/tv_activity.xml
new file mode 100644
index 00000000..cfa1f6ba
--- /dev/null
+++ b/ui/src/main/res/layout/tv_activity.xml
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <data>
+
+ <import type="android.view.View" />
+
+ <import type="com.wireguard.android.model.ObservableTunnel" />
+
+ <import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
+
+ <variable
+ name="isDeleting"
+ type="androidx.databinding.ObservableBoolean" />
+
+ <variable
+ name="files"
+ type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, KeyedFile&gt;" />
+
+ <variable
+ name="filesRoot"
+ type="androidx.databinding.ObservableField&lt;String&gt;" />
+
+ <variable
+ name="tunnels"
+ type="com.wireguard.android.databinding.ObservableKeyedArrayList&lt;String, ObservableTunnel&gt;" />
+
+ <variable
+ name="tunnelRowConfigurationHandler"
+ type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
+
+ <variable
+ name="filesRowConfigurationHandler"
+ type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
+ </data>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/banner_logo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginStart="5dp"
+ android:layout_marginTop="5dp"
+ app:cardElevation="2dp"
+ app:contentPadding="0dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ImageView
+ android:layout_width="320dp"
+ android:layout_height="67dp"
+ android:contentDescription="@string/app_name"
+ android:scaleType="fitXY"
+ app:srcCompat="@drawable/tv_logo_banner" />
+
+ </com.google.android.material.card.MaterialCardView>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/tunnel_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal"
+ android:visibility="@{(tunnels.isEmpty || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
+ app:configurationHandler="@{tunnelRowConfigurationHandler}"
+ app:items="@{tunnels}"
+ app:layout="@{@layout/tv_tunnel_list_item}"
+ app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+ app:layout_constraintBottom_toTopOf="@id/delete_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/banner_logo"
+ app:spanCount="3"
+ tools:itemCount="10"
+ tools:listitem="@layout/tv_tunnel_list_item" />
+
+ <TextView
+ android:id="@+id/files_root_label"
+ 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"
+ tools:visibility="gone" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/files_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal"
+ android:visibility="@{filesRoot.isEmpty ? View.GONE : View.VISIBLE}"
+ app:configurationHandler="@{filesRowConfigurationHandler}"
+ app:items="@{files}"
+ app:layout="@{@layout/tv_file_list_item}"
+ app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
+ app:layout_constraintBottom_toTopOf="@id/import_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/files_root_label"
+ app:spanCount="5"
+ tools:itemCount="10"
+ tools:listitem="@layout/tv_file_list_item"
+ tools:visibility="gone" />
+
+ <TextView
+ 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"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/banner_logo"
+ tools:visibility="gone" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/import_button"
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:minWidth="0dp"
+ android:visibility="@{isDeleting ? View.GONE : View.VISIBLE}"
+ app:icon="@{filesRoot.isEmpty ? @drawable/ic_action_add_white : @drawable/ic_arrow_back}"
+ app:iconPadding="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/delete_button"
+ style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:minWidth="0dp"
+ android:visibility="@{((tunnels.isEmpty &amp;&amp; !isDeleting) || !filesRoot.isEmpty) ? View.GONE : View.VISIBLE}"
+ app:icon="@{isDeleting ? @drawable/ic_arrow_back : @drawable/ic_action_delete}"
+ app:iconPadding="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
diff --git a/ui/src/main/res/layout/tv_file_list_item.xml b/ui/src/main/res/layout/tv_file_list_item.xml
new file mode 100644
index 00000000..d5afa0a4
--- /dev/null
+++ b/ui/src/main/res/layout/tv_file_list_item.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <data>
+
+ <import type="com.wireguard.android.activity.TvMainActivity.KeyedFile" />
+
+ <variable
+ name="key"
+ type="String" />
+
+ <variable
+ name="item"
+ type="KeyedFile" />
+ </data>
+
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="320dp"
+ android:layout_height="50dp"
+ android:layout_margin="8dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="0dp"
+ android:checkable="true"
+ android:focusable="true"
+ app:contentPadding="8dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@{key}"
+ android:textAppearance="?attr/textAppearanceTitleLarge"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.google.android.material.card.MaterialCardView>
+
+</layout>
diff --git a/ui/src/main/res/layout/tv_tunnel_list_item.xml b/ui/src/main/res/layout/tv_tunnel_list_item.xml
new file mode 100644
index 00000000..2d6039f1
--- /dev/null
+++ b/ui/src/main/res/layout/tv_tunnel_list_item.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <data>
+
+ <import type="android.view.View" />
+
+ <import type="com.wireguard.android.model.ObservableTunnel" />
+
+ <import type="com.wireguard.android.backend.Tunnel.State" />
+
+ <variable
+ name="isDeleting"
+ type="androidx.databinding.ObservableBoolean" />
+
+ <variable
+ name="isFocused"
+ type="androidx.databinding.ObservableBoolean" />
+
+ <variable
+ name="key"
+ type="String" />
+
+ <variable
+ name="item"
+ type="com.wireguard.android.model.ObservableTunnel" />
+ </data>
+
+ <com.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="@color/tv_list_item_tint"
+ android:checkable="true"
+ android:focusable="true"
+ app:contentPadding="8dp"
+ app:isDeleting="@{isDeleting}"
+ app:isUp="@{item.state == State.UP}">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/tunnel_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@{item.name}"
+ 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"
+ 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"
+ tools:text="rx: 200 MB, tx: 100 MB" />
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/tunnel_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/tv_delete"
+ android:visibility="@{(isDeleting &amp;&amp; isFocused) ? View.VISIBLE : View.GONE}"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:visibility="gone" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.wireguard.android.widget.TvCardView>
+
+</layout>
diff --git a/app/src/main/res/menu/config_editor.xml b/ui/src/main/res/menu/config_editor.xml
index dd0137df..dd0137df 100644
--- a/app/src/main/res/menu/config_editor.xml
+++ b/ui/src/main/res/menu/config_editor.xml
diff --git a/ui/src/main/res/menu/log_viewer.xml b/ui/src/main/res/menu/log_viewer.xml
new file mode 100644
index 00000000..64b4be0a
--- /dev/null
+++ b/ui/src/main/res/menu/log_viewer.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/save_log"
+ android:icon="@drawable/ic_action_save"
+ android:title="@string/log_export_title"
+ app:showAsAction="ifRoom" />
+</menu>
diff --git a/app/src/main/res/menu/main_activity.xml b/ui/src/main/res/menu/main_activity.xml
index 68bce52e..68bce52e 100644
--- a/app/src/main/res/menu/main_activity.xml
+++ b/ui/src/main/res/menu/main_activity.xml
diff --git a/app/src/main/res/menu/tunnel_detail.xml b/ui/src/main/res/menu/tunnel_detail.xml
index 2834a661..2834a661 100644
--- a/app/src/main/res/menu/tunnel_detail.xml
+++ b/ui/src/main/res/menu/tunnel_detail.xml
diff --git a/app/src/main/res/menu/tunnel_list_action_mode.xml b/ui/src/main/res/menu/tunnel_list_action_mode.xml
index 22f61943..22f61943 100644
--- a/app/src/main/res/menu/tunnel_list_action_mode.xml
+++ b/ui/src/main/res/menu/tunnel_list_action_mode.xml
diff --git a/app/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/app/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/app/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/app/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/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ui/src/main/res/mipmap-hdpi/ic_launcher.png
index 8a5b8d69..8a5b8d69 100644
--- a/app/src/main/res/mipmap-hdpi/ic_launcher.png
+++ b/ui/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 56111942..56111942 100644
--- a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
+++ b/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ui/src/main/res/mipmap-mdpi/ic_launcher.png
index aa5ac825..aa5ac825 100644
--- a/app/src/main/res/mipmap-mdpi/ic_launcher.png
+++ b/ui/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png
index ebad3192..ebad3192 100644
--- a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
+++ b/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/ui/src/main/res/mipmap-xhdpi/banner.png b/ui/src/main/res/mipmap-xhdpi/banner.png
new file mode 100644
index 00000000..2cceb821
--- /dev/null
+++ b/ui/src/main/res/mipmap-xhdpi/banner.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ui/src/main/res/mipmap-xhdpi/ic_launcher.png
index ef183c3d..ef183c3d 100644
--- a/app/src/main/res/mipmap-xhdpi/ic_launcher.png
+++ b/ui/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 79f3fa98..79f3fa98 100644
--- a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
+++ b/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 95e7241c..95e7241c 100644
--- a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+++ b/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index d1731b3b..d1731b3b 100644
--- a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
+++ b/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index d3ec336b..d3ec336b 100644
--- a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index c0ae03bc..c0ae03bc 100644
--- a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
+++ b/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
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
new file mode 100644
index 00000000..73f6111a
--- /dev/null
+++ b/ui/src/main/res/values-ar-rSA/strings.xml
@@ -0,0 +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>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="zero">تم تحديد %d من الأنفاق</item>
+ <item quantity="one">تم تحديد %d من الأنفاق</item>
+ <item quantity="two">تم تحديد %d من الأنفاق</item>
+ <item quantity="few">تم تحديد %d من الأنفاق</item>
+ <item quantity="many">تم تحديد %d من الأنفاق</item>
+ <item quantity="other">تم تحديد %d من الأنفاق</item>
+ </plurals>
+ <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
new file mode 100644
index 00000000..073bb798
--- /dev/null
+++ b/ui/src/main/res/values-ca-rES/strings.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">No s\'ha pogut esborrar %d túnel: %s</item>
+ <item quantity="other">No s\'han pogut esborrar %d túnels: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">El túnel %d s\'ha eliminat correctament</item>
+ <item quantity="other">Els túnels %d s\'han eliminat correctament</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d túnel seleccionat</item>
+ <item quantity="other">%d túnels seleccionats</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Túnel %1$d de %2$d importat</item>
+ <item quantity="other">Túnels %1$d de %2$d importats</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d túnel importat</item>
+ <item quantity="other">%d túnels importats</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d Aplicació exclosa</item>
+ <item quantity="other">%d Aplicacins excloses</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Aplicació inclosa</item>
+ <item quantity="other">%d Aplicacions incloses</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d exclòs</item>
+ <item quantity="other">%d exclosos</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inclòs</item>
+ <item quantity="other">%d inclosos</item>
+ </plurals>
+ <string name="all_applications">Totes les aplicacions</string>
+ <string name="exclude_from_tunnel">Exclou</string>
+ <string name="include_in_tunnel">Inclou només</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Inclou %d aplicació</item>
+ <item quantity="other">Inclou %d aplicacions</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Exclou %d aplicació</item>
+ <item quantity="other">Exclou %d aplicacions</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">cada segon</item>
+ <item quantity="other">cada %d segons</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">segon</item>
+ <item quantity="other">segons</item>
+ </plurals>
+ <string name="use_all_applications">Utilitza totes les aplicacions</string>
+ <string name="add_peer">Afegir parell</string>
+ <string name="addresses">Adreces</string>
+ <string name="applications">Aplicacions</string>
+ <string name="allow_remote_control_intents_summary_off">Aplicacions externes no poden cambiar túnels (recomanat)</string>
+ <string name="allow_remote_control_intents_summary_on">Aplicacions externes poden cambiar túnels (avançat)</string>
+ <string name="allow_remote_control_intents_title">Permet aplicacions de control remot</string>
+ <string name="allowed_ips">IPs permeses</string>
+ <string name="bad_config_context">%1$s de %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s en %2$s</string>
+ <string name="bad_config_explanation_pka">: Ha de ser positiu i no superior a 65535</string>
+ <string name="bad_config_explanation_positive_number">: Ha de ser positiu</string>
+ <string name="bad_config_explanation_udp_port">: Ha de ser un nombre vàlid de port UDP</string>
+ <string name="bad_config_reason_invalid_key">Clau no vàlida</string>
+ <string name="bad_config_reason_invalid_number">Número no vàlid</string>
+ <string name="bad_config_reason_invalid_value">Valor no vàlid</string>
+ <string name="bad_config_reason_missing_attribute">Falta atribut</string>
+ <string name="bad_config_reason_missing_section">Falta secció</string>
+ <string name="bad_config_reason_syntax_error">Error de sintaxi</string>
+ <string name="bad_config_reason_unknown_attribute">Atribut desconegut</string>
+ <string name="bad_config_reason_unknown_section">Secció desconeguda</string>
+ <string name="bad_config_reason_value_out_of_range">Valor fora de rang</string>
+ <string name="bad_extension_error">El fitxer ha de ser .conf o .zip</string>
+ <string name="cancel">Cancel·la</string>
+ <string name="config_delete_error">No es pot esborrar fitxer de configuració %s</string>
+ <string name="config_exists_error">Configuració per \"%s\" ja existeix</string>
+ <string name="config_file_exists_error">Fitxer de configuració \"%s\" ja existeix</string>
+ <string name="config_not_found_error">No s\'ha trobat el fitxer de configuració \"%s\"</string>
+ <string name="config_rename_error">No es pot cambiar el nom del fitxer de configuració \"%s\"</string>
+ <string name="config_save_error">No es pot guardar la configuraxió per \"%1$s\": %2$s</string>
+ <string name="config_save_success">La configuració per \"%s\" s\'ha guadat correctament</string>
+ <string name="create_activity_title">Crear túnel WireGuard</string>
+ <string name="create_bin_dir_error">No s\'ha pogut crear la carpeta local pels binaris</string>
+ <string name="create_downloads_file_error">No es pot crear arxiu en la carpeta de descàrregues</string>
+ <string name="create_empty">Crea des de zero</string>
+ <string name="create_from_file">Importa des de fitxer o arxiu</string>
+ <string name="create_from_qr_code">Escaneja codi QR</string>
+ <string name="create_output_dir_error">Incapaç de crear directori de sortida</string>
+ <string name="create_temp_dir_error">No s’ha pogut crear el directori local temporal</string>
+ <string name="create_tunnel">Crear túnel</string>
+ <string name="copied_to_clipboard">S\'ha copiat %s al portaretalls</string>
+ <string name="dark_theme_summary_off">Actualment fent servir el tema clar (dia)</string>
+ <string name="dark_theme_summary_on">Actualment fent servir el tema fosc (nit)</string>
+ <string name="dark_theme_title">Utilitza tema fosc</string>
+ <string name="delete">Elimina</string>
+ <string name="tv_delete">Slecciona túnel a esborrar</string>
+ <string name="tv_select_a_storage_drive">Seleccioneu un disc d\'emmagatzematge</string>
+ <string name="tv_no_file_picker">Si us plau, instsleu un gestor de fitxers per navegar pels arxius</string>
+ <string name="tv_add_tunnel_get_started">Afegeix un túnel per començar</string>
+ <string name="disable_config_export_title">Desactiva l\'exportació de configuracions</string>
+ <string name="disable_config_export_description">Desactivar l\'exportacio de configuracions fa que les claus privades siguin menys accessibles</string>
+ <string name="dns_servers">Servidors DNS</string>
+ <string name="edit">Edita</string>
+ <string name="endpoint">Extrem</string>
+ <string name="error_down">Error desactivant el túnel: %s</string>
+ <string name="error_fetching_apps">Error obtenint llista d\'aplicacions: %s</string>
+ <string name="error_root">Si us plau, obteniu accés root i torneu a intentar</string>
+ <string name="error_up">Error activant túnel: %s</string>
+ <string name="exclude_private_ips">Exclou IPs privades</string>
+ <string name="generate_new_private_key">Genera nova clau privada</string>
+ <string name="generic_error">Error “%s” desconegut</string>
+ <string name="hint_automatic">(automàtic)</string>
+ <string name="hint_generated">(generat)</string>
+ <string name="hint_optional">(opcional)</string>
+ <string name="hint_optional_discouraged">(opcional, no recomanat)</string>
+ <string name="hint_random">(aleatori)</string>
+ <string name="illegal_filename_error">Nom de fitxer no vàlid \"%s\"</string>
+ <string name="import_error">No s\'ha pogut importar el túnel: %s</string>
+ <string name="import_from_qr_code">Importa túnel desde codi QR</string>
+ <string name="import_success">Importat “%s”</string>
+ <string name="interface_title">Interfície</string>
+ <string name="key_contents_error">Caràcters incorrectes en la clau</string>
+ <string name="key_length_error">Longitud de clau incorrecta</string>
+ <string name="key_length_explanation_base64">: Les claus en base64 a WireGuard han de tenir 44 caràcters (32 bytes)</string>
+ <string name="key_length_explanation_binary">: Les claus WireGuard han de ser de 32 bytes</string>
+ <string name="key_length_explanation_hex">: Les claus en hexadecimal a WireGuard han de tenir 64 caràcters (32 bytes)</string>
+ <string name="listen_port">Port d\'escolta</string>
+ <string name="log_export_error">No s\'ha estat capaç d\'exportar el registre: %s</string>
+ <string name="log_export_subject">WireGuard Android Log File</string>
+ <string name="log_export_success">Guardat a \"%s\"</string>
+ <string name="log_export_title">Exporta el registre</string>
+ <string name="log_saver_activity_label">Guarda registre</string>
+ <string name="log_viewer_pref_summary">El registre pot ajudar a solucionar errors</string>
+ <string name="log_viewer_pref_title">Mostra el registre d\'aplicació</string>
+ <string name="log_viewer_title">Registre</string>
+ <string name="logcat_error">No es pot executar logcat: </string>
+ <string name="module_enabler_disabled_summary">El mòdul experimental del kernel pot millorar el rendiment</string>
+ <string name="module_enabler_disabled_title">Activa el backend del mòdul del kernel</string>
+ <string name="module_enabler_enabled_title">Desactiva el backend del mòdul del kernel</string>
+ <string name="module_installer_error">Alguna cosa ha anat malament. Si us plau, prova de nou</string>
+ <string name="module_installer_not_found">El vostre dispositiu no té mòduls disponibles</string>
+ <string name="module_installer_title">Descàrega i instala el mòdul kernel</string>
+ <string name="module_installer_working">Descarregant i instalant…</string>
+ <string name="module_version_error">No s\'ha pogut determinar la versió del mòdul del kernel</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Activar un túnel desactivarà els altres</string>
+ <string name="multiple_tunnels_summary_on">Múltiples túnels es podran activar a la vegada</string>
+ <string name="multiple_tunnels_title">Permet múltiples túnels simultanis</string>
+ <string name="name">Nom</string>
+ <string name="no_config_error">Intentant activar un túnel que no té configuració</string>
+ <string name="no_configs_error">No s\'ha trobat cap configuració</string>
+ <string name="no_tunnels_error">No existeixen túnels</string>
+ <string name="parse_error_inet_address">Adreça IP</string>
+ <string name="parse_error_inet_endpoint">extrem</string>
+ <string name="parse_error_inet_network">Xarxa IP</string>
+ <string name="parse_error_integer">número</string>
+ <string name="parse_error_reason">No es pot analitzar %1$s \"%2$s\"</string>
+ <string name="peer">Parell</string>
+ <string name="permission_description">controla els túnels de WireGuard, habilitant i deshabilitant túnels a discreció, potencialment desviant el trànsit</string>
+ <string name="permission_label">controla els túnels WireGuard</string>
+ <string name="persistent_keepalive">Missatge de persistència</string>
+ <string name="pre_shared_key">Clau precompartida</string>
+ <string name="pre_shared_key_enabled">activat</string>
+ <string name="private_key">Clau privada</string>
+ <string name="public_key">Clau pública</string>
+ <string name="qr_code_hint">Consell: generar amb `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="restore_on_boot_summary_off">No s\'activaran els túnels seleccionats al arrancar el sistema</string>
+ <string name="restore_on_boot_summary_on">S\'activaran els túnels seleccionats al arrancar el sistema</string>
+ <string name="restore_on_boot_title">Restableix a l\'inici</string>
+ <string name="save">Guarda</string>
+ <string name="select_all">Selecciona-ho tot</string>
+ <string name="settings">Configuració</string>
+ <string name="shell_exit_status_read_error">La shell no pot llegit l\'estat de sortida</string>
+ <string name="shell_marker_count_error">La shell esperava 4 marcadors, n\'ha rebut %d</string>
+ <string name="shell_start_error">No s\'ha pogut iniciar: %d</string>
+ <string name="success_application_will_restart">Èxit. Ara l\'aplicació es reiniciarà…</string>
+ <string name="toggle_all">Canviar tot</string>
+ <string name="toggle_error">Error al canviar el túnel WireGuard: %s</string>
+ <string name="tools_installer_already">wg i wg-quick ja estan instalats</string>
+ <string name="tools_installer_failure">No s\'ha pogut instalar les eines de linia de comandes (ets root?)</string>
+ <string name="tools_installer_success_magisk">wg i wg-quick instalats com a mòdul de Magisk (es necessita reinicar)</string>
+ <string name="tools_installer_success_system">wg i wg-quick instalats a la partició de sistema</string>
+ <string name="tools_installer_title">Instala les eines de la línia d\'ordres</string>
+ <string name="tools_installer_working">Instalant wg i wg-quick</string>
+ <string name="tools_unavailable_error">Eines necessàries no disponibles</string>
+ <string name="transfer">Transferir</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">No s\'ha pogut crear el dispositiu túnel</string>
+ <string name="tunnel_config_error">No s\'ha pigut confirmgurar el túnel (wg-quick ha tornat %d)</string>
+ <string name="tunnel_create_error">No s\'ha pogut crear túnel: %s</string>
+ <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_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>
+ <string name="tunnel_rename_error">No s\'ha pogut renombrar el túnel: %s</string>
+ <string name="tunnel_rename_success">El nom del túnel s\'ha canviat correctament a \"%s\"</string>
+ <string name="type_name_kernel_module">Mòdul del kernel</string>
+ <string name="unknown_error">Error desconegut</string>
+ <string name="version_summary_unknown">Versió de %s desconeguda</string>
+ <string name="version_title">WireGuard per a Android v%s</string>
+ <string name="vpn_not_authorized_error">Servei de VPN no autoritzat per l\'usuari</string>
+ <string name="vpn_start_error">No s\'ha pogut iniciar el servei de VPN de Android</string>
+ <string name="zip_export_error">No s\'han pogut exportar els túnels: %s</string>
+ <string name="zip_export_success">Guardat a \"%s\"</string>
+ <string name="zip_export_summary">El fitxer zip es guardarà a la carpeta de descàrregues</string>
+ <string name="zip_export_title">Exporta els túnels a un fitxer zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Autentifiqueu-vos per exportar els túnels</string>
+ <string name="biometric_prompt_private_key_title">Autentifiqueu-vos per veure la clau privada</string>
+ <string name="biometric_auth_error">Error d\'autenticació</string>
+ <string name="biometric_auth_error_reason">Error d\'autenticació: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-cs-rCZ/strings.xml b/ui/src/main/res/values-cs-rCZ/strings.xml
new file mode 100644
index 00000000..3b50d711
--- /dev/null
+++ b/ui/src/main/res/values-cs-rCZ/strings.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d tunel nelze smazat: %s</item>
+ <item quantity="few">%d tunely nelze smazat: %s</item>
+ <item quantity="many">%d tunelů nelze smazat: %s</item>
+ <item quantity="other">%d tunelů nelze smazat: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tunel byl smazán</item>
+ <item quantity="few">%d tunely byly smazány</item>
+ <item quantity="many">%d tunelů bylo smazáno</item>
+ <item quantity="other">%d tunelů bylo smazáno</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">Vybrán %d tunel</item>
+ <item quantity="few">Vybrány %d tunely</item>
+ <item quantity="many">Vybráno %d tunelů</item>
+ <item quantity="other">Vybráno %d tunelů</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importován %1$d z %2$d tunelů</item>
+ <item quantity="few">Importovány %1$d z %2$d tunelů</item>
+ <item quantity="many">Importováno %1$d z %2$d tunelů</item>
+ <item quantity="other">Importováno %1$d z %2$d tunelů</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Importován %d tunel</item>
+ <item quantity="few">Importovány %d tunely</item>
+ <item quantity="many">Importováno %d tunelů</item>
+ <item quantity="other">Importováno %d tunelů</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d vyloučená aplikace</item>
+ <item quantity="few">%d vyloučené aplikace</item>
+ <item quantity="many">%d vyloučených aplikací</item>
+ <item quantity="other">%d vyloučených aplikací</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d zahrnutá aplikace</item>
+ <item quantity="few">%d zahrnuté aplikace</item>
+ <item quantity="many">%d zahrnutých aplikací</item>
+ <item quantity="other">%d zahrnutých aplikací</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d vyloučena</item>
+ <item quantity="few">%d vyloučeny</item>
+ <item quantity="many">%d vyloučeno</item>
+ <item quantity="other">%d vyloučeno</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d zahrnuta</item>
+ <item quantity="few">%d zahrnuty</item>
+ <item quantity="many">%d zahrnuto</item>
+ <item quantity="other">%d zahrnuto</item>
+ </plurals>
+ <string name="all_applications">Všechny aplikace</string>
+ <string name="exclude_from_tunnel">Vyloučit</string>
+ <string name="include_in_tunnel">Zahrnout pouze</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Zahrnout %d aplikaci</item>
+ <item quantity="few">Zahrnout %d aplikace</item>
+ <item quantity="many">Zahrnout %d aplikací</item>
+ <item quantity="other">Zahrnout %d aplikací</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Vyloučit %d aplikaci</item>
+ <item quantity="few">Vyloučit %d aplikace</item>
+ <item quantity="many">Vyloučit %d aplikací</item>
+ <item quantity="other">Vyloučit %d aplikací</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">každou sekundu</item>
+ <item quantity="few">každé %d sekundy</item>
+ <item quantity="many">každých %d sekund</item>
+ <item quantity="other">každých %d sekund</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">sekunda</item>
+ <item quantity="few">sekundy</item>
+ <item quantity="many">sekund</item>
+ <item quantity="other">sekund</item>
+ </plurals>
+ <string name="use_all_applications">Použít všechny aplikace</string>
+ <string name="add_peer">Přidat peera</string>
+ <string name="addresses">Adresy</string>
+ <string name="applications">Aplikace</string>
+ <string name="allow_remote_control_intents_summary_off">Externí aplikace nemohou přepínat tunely (doporučeno)</string>
+ <string name="allow_remote_control_intents_summary_on">Externí aplikace mohou přepínat tunely (pokročilé)</string>
+ <string name="allow_remote_control_intents_title">Povolit aplikace vzdálené správy</string>
+ <string name="allowed_ips">Povolené IP</string>
+ <string name="bad_config_context">%2$s položky %1$s</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í být kladný a maximálně 65535</string>
+ <string name="bad_config_explanation_positive_number">: Musí být kladný</string>
+ <string name="bad_config_explanation_udp_port">: Musí být platné číslo UDP portu</string>
+ <string name="bad_config_reason_invalid_key">Neplatný klíč</string>
+ <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">Chybějící atribut</string>
+ <string name="bad_config_reason_missing_section">Chybějící sekce</string>
+ <string name="bad_config_reason_syntax_error">Syntaktická chyba</string>
+ <string name="bad_config_reason_unknown_attribute">Neznámý atribut</string>
+ <string name="bad_config_reason_unknown_section">Neznámá sekce</string>
+ <string name="bad_config_reason_value_out_of_range">Hodnota je mimo rozsah</string>
+ <string name="bad_extension_error">Soubor musí být .conf nebo .zip</string>
+ <string name="cancel">Zrušit</string>
+ <string name="config_delete_error">Nelze smazat konfigurační soubor %s</string>
+ <string name="config_exists_error">Konfigurace pro „%s“ již existuje</string>
+ <string name="config_file_exists_error">Konfigurační soubor „%s“ již existuje</string>
+ <string name="config_not_found_error">Konfigurační soubor „%s“ nebyl nalezen</string>
+ <string name="config_rename_error">Konfigurační soubor „%s“ nelze přejmenovat</string>
+ <string name="config_save_error">Nelze uložit konfiguraci pro „%1$s“: %2$s</string>
+ <string name="config_save_success">Konfigurace pro „%s“ byla úspěšně uložena</string>
+ <string name="create_activity_title">Vytvořit WireGuard tunel</string>
+ <string name="delete">Smazat</string>
+ <string name="log_viewer_pref_summary">Logy mohou pomoci s debuggingem</string>
+ <string name="log_viewer_pref_title">Zobrazit log aplikace</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Nelze spustit logcat: </string>
+ <string name="module_enabler_disabled_summary">Experimentální kernel modul může zlepšit výkon</string>
+ <string name="module_enabler_disabled_title">Povolit backend kernel modulu</string>
+ <string name="module_enabler_enabled_title">Vypnout backend kernel modulu</string>
+ <string name="module_installer_error">Něco se pokazilo. Zkuste to prosím znovu</string>
+ <string name="module_installer_initial">Experimentální kernel modul může zlepšit výkon</string>
+ <string name="module_installer_not_found">Pro toto zařízení nejsou dostupné žádné moduly</string>
+ <string name="module_installer_title">Stáhnout a nainstalovat kernel modul</string>
+ <string name="module_installer_working">Stahování a instalace…</string>
+ <string name="module_version_error">Nelze určit verzi kernel modulu</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Zapnutí jednoho tunelu vypne ostatní tunely</string>
+ <string name="multiple_tunnels_summary_on">Více tunelů může být zapnuto najednou</string>
+ <string name="multiple_tunnels_title">Povolit více simultánních tunelů</string>
+ <string name="name">Název</string>
+</resources>
diff --git a/ui/src/main/res/values-da-rDK/strings.xml b/ui/src/main/res/values-da-rDK/strings.xml
new file mode 100644
index 00000000..bf786c07
--- /dev/null
+++ b/ui/src/main/res/values-da-rDK/strings.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Kan ikke slette %d tunnel: %s</item>
+ <item quantity="other">Ikke i stand til at slette %d tunneler: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Slettede %d tunnel</item>
+ <item quantity="other">%d tunneller blev slettet</item>
+ </plurals>
+ <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
new file mode 100644
index 00000000..372abcad
--- /dev/null
+++ b/ui/src/main/res/values-de/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d Tunnel konnten nicht gelöscht werden: %s</item>
+ <item quantity="other">%d Tunnel konnten nicht gelöscht werden: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d Tunnel erfolgreich gelöscht</item>
+ <item quantity="other">%d Tunnel erfolgreich gelöscht</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d Tunnel ausgewählt</item>
+ <item quantity="other">%d Tunnel ausgewählt</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d von %2$d Tunnel importiert</item>
+ <item quantity="other">%1$d von %2$d Tunnel importiert</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d Tunnel importiert</item>
+ <item quantity="other">%d Tunnel importiert</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d ausgeschlossene Anwendung</item>
+ <item quantity="other">%d ausgeschlossene Anwendungen</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d einbezogene Anwendung</item>
+ <item quantity="other">%d einbezogene Anwendungen</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d ausgeschlossen</item>
+ <item quantity="other">%d ausgeschlossen</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d einbezogen</item>
+ <item quantity="other">%d einbezogen</item>
+ </plurals>
+ <string name="all_applications">Alle Anwendungen</string>
+ <string name="exclude_from_tunnel">Ausschließen</string>
+ <string name="include_in_tunnel">Nur einbeziehen</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">%d App einbeziehen</item>
+ <item quantity="other">%d App einbeziehen</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">%d App ausschließen</item>
+ <item quantity="other">%d App ausschließen</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">jede Sekunde</item>
+ <item quantity="other">alle %d Sekunden</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">Sekunde</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>
+ <string name="addresses">Adressen</string>
+ <string name="applications">Anwendungen</string>
+ <string name="allow_remote_control_intents_summary_off">Externe Apps dürfen keine Tunnel umschalten (empfohlen)</string>
+ <string name="allow_remote_control_intents_summary_on">Externe Apps dürfen Tunnel umschalten (erweitert)</string>
+ <string name="allow_remote_control_intents_title">Erlaube Steuerung über externe Apps</string>
+ <string name="allowed_ips">Erlaubte IPs</string>
+ <string name="bad_config_context">%2$s des %1$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">: Muss positiv sein und nicht größer als 65535</string>
+ <string name="bad_config_explanation_positive_number">: Muss positiv sein</string>
+ <string name="bad_config_explanation_udp_port">: Muss eine gültige UDP-Portnummer sein</string>
+ <string name="bad_config_reason_invalid_key">Ungültiger Schlüssel</string>
+ <string name="bad_config_reason_invalid_number">Ungültige Zahl</string>
+ <string name="bad_config_reason_invalid_value">Ungültiger Wert</string>
+ <string name="bad_config_reason_missing_attribute">Fehlendes Attribut</string>
+ <string name="bad_config_reason_missing_section">Fehlender Abschnitt</string>
+ <string name="bad_config_reason_syntax_error">Syntaxfehler</string>
+ <string name="bad_config_reason_unknown_attribute">Unbekanntes Attribut</string>
+ <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>
+ <string name="config_file_exists_error">Konfigurationsdatei „%s“ existiert bereits</string>
+ <string name="config_not_found_error">Konfigurationsdatei „%s“ nicht gefunden</string>
+ <string name="config_rename_error">Konfigurationsdatei „%s “ kann nicht umbenannt werden</string>
+ <string name="config_save_error">Konfiguration für „%1$s“ kann nicht gespeichert werden: %2$s</string>
+ <string name="config_save_success">Konfiguration für „%s ” erfolgreich gespeichert</string>
+ <string name="create_activity_title">WireGuard-Tunnel erstellen</string>
+ <string name="create_bin_dir_error">Lokales Anwendungsverzeichnis kann nicht erstellt werden</string>
+ <string name="create_downloads_file_error">Datei kann im Download-Verzeichnis nicht erstellt werden</string>
+ <string name="create_empty">Neu erstellen</string>
+ <string name="create_from_file">Aus Datei oder Archiv importieren</string>
+ <string name="create_from_qr_code">Von QR-Code scannen</string>
+ <string name="create_output_dir_error">Konnte das Ausgabeverzeichnis nicht erstellen</string>
+ <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">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>
+ <string name="generic_error">Unbekannter „%s“ Fehler</string>
+ <string name="hint_automatic">(auto)</string>
+ <string name="hint_generated">(erzeugt)</string>
+ <string name="hint_optional">(optional)</string>
+ <string name="hint_optional_discouraged">(optional, nicht empfohlen)</string>
+ <string name="hint_random">(zufällig)</string>
+ <string name="illegal_filename_error">Ungültiger Dateiname „%s“</string>
+ <string name="import_error">Kann Tunnel nicht importieren: %s</string>
+ <string name="import_from_qr_code">Tunnel aus QR-Code importieren</string>
+ <string name="import_success">„%s” importiert</string>
+ <string name="interface_title">Interface</string>
+ <string name="key_contents_error">Unzulässige Zeichen im Schlüssel</string>
+ <string name="key_length_error">Falsche Schlüssellänge</string>
+ <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>
+ <string name="log_export_success">Gespeichert in „%s”</string>
+ <string name="log_export_title">Logdatei exportieren</string>
+ <string name="log_saver_activity_label">Protokoll speichern</string>
+ <string name="log_viewer_pref_summary">Protokolle können beim Debuggen helfen</string>
+ <string name="log_viewer_pref_title">Anwendungs-Protokoll anzeigen</string>
+ <string name="log_viewer_title">Protokoll</string>
+ <string name="logcat_error">Konnte logcat nicht ausführen: </string>
+ <string name="module_enabler_disabled_summary">Das experimentelle Kernelmodul kann die Leistung verbessern</string>
+ <string name="module_enabler_disabled_title">Kernelmodul-Backend aktivieren</string>
+ <string name="module_enabler_enabled_summary">Das langsamere Userspace-Backend kann die Stabilität verbessern</string>
+ <string name="module_enabler_enabled_title">Kernelmodul-Backend deaktivieren</string>
+ <string name="module_installer_error">Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut</string>
+ <string name="module_installer_initial">Das experimentelle Kernelmodul kann die Leistung verbessern</string>
+ <string name="module_installer_not_found">Für Ihr Gerät sind keine Module verfügbar</string>
+ <string name="module_installer_title">Kernelmodul herunterladen und installieren</string>
+ <string name="module_installer_working">Lade herunter und installiere…</string>
+ <string name="module_version_error">Konnte Version des Kernel-Moduls nicht ermitteln</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Das einschalten eines Tunnels wird andere ausschalten</string>
+ <string name="multiple_tunnels_summary_on">Mehrere Tunnel können gleichzeitig aktiviert sein</string>
+ <string name="multiple_tunnels_title">Erlaube mehrere gleichzeitige Tunnel</string>
+ <string name="name">Name</string>
+ <string name="no_config_error">Es wurde versucht, einen Tunnel ohne Konfiguration zu starten</string>
+ <string name="no_configs_error">Keine Konfigurationen gefunden</string>
+ <string name="no_tunnels_error">Keine Tunnel vorhanden</string>
+ <string name="parse_error_generic">String (Zeichenkette)</string>
+ <string name="parse_error_inet_address">IP-Adresse</string>
+ <string name="parse_error_inet_endpoint">Endpunkt</string>
+ <string name="parse_error_inet_network">IP-Netzwerk</string>
+ <string name="parse_error_integer">Zahl</string>
+ <string name="parse_error_reason">Kann %1$s “%2$s ” nicht verarbeiten</string>
+ <string name="peer">Teilnehmer</string>
+ <string name="permission_description">WireGuard-Tunnel steuern, Tunnel aktivieren und nach Belieben deaktivieren, wodurch der Internetverkehr möglicherweise falsch geleitet wird</string>
+ <string name="permission_label">WireGuard-Tunnel steuern</string>
+ <string name="persistent_keepalive">Dauerhafte Erhaltung</string>
+ <string name="pre_shared_key">Vorab geteilter Schlüssel</string>
+ <string name="pre_shared_key_enabled">aktiviert</string>
+ <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>
+ <string name="save">Speichern</string>
+ <string name="select_all">Alle auswählen</string>
+ <string name="settings">Einstellungen</string>
+ <string name="shell_exit_status_read_error">Shell kann den Exit-Status nicht lesen</string>
+ <string name="shell_marker_count_error">Die Shell erwartete 4 Marker, erhielt aber %d</string>
+ <string name="shell_start_error">Shell konnte nicht gestartet werden: %d</string>
+ <string name="success_application_will_restart">Erfolgreich. Die Anwendung wird nun neu starten…</string>
+ <string name="toggle_all">Alle umschalten</string>
+ <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 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 Werkzeuge nicht verfügbar</string>
+ <string name="transfer">Transfer</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">empfangen: %1$s, gesendet: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Konnte tun Gerät nicht erstellen</string>
+ <string name="tunnel_config_error">Tunnel konnte nicht konfiguriert werden (wg-quick gab %d zurück)</string>
+ <string name="tunnel_create_error">Kann Tunnel nicht erstellen: %s</string>
+ <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 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>
+ <string name="tunnel_rename_error">Kann Tunnel nicht umbenennen: %s</string>
+ <string name="tunnel_rename_success">Tunnel erfolgreich in „%s “ umbenannt</string>
+ <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>
+ <string name="vpn_not_authorized_error">VPN-Dienst nicht vom Benutzer autorisiert</string>
+ <string name="vpn_start_error">Android VPN-Dienst konnte nicht gestartet werden</string>
+ <string name="zip_export_error">Kann Tunnel nicht exportieren: %s</string>
+ <string name="zip_export_success">Gespeichert unter “%s”</string>
+ <string name="zip_export_summary">Zip-Datei wird im Download-Verzeichnis gespeichert</string>
+ <string name="zip_export_title">Tunnel als Zip-Datei exportieren</string>
+ <string name="biometric_prompt_zip_exporter_title">Authentifizieren, um Tunnel zu exportieren</string>
+ <string name="biometric_prompt_private_key_title">Authentifizieren, um privaten Schlüssel anzuzeigen</string>
+ <string name="biometric_auth_error">Authentifizierungsfehler</string>
+ <string name="biometric_auth_error_reason">Authentifizierungsfehler: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-el-rGR/strings.xml b/ui/src/main/res/values-el-rGR/strings.xml
new file mode 100644
index 00000000..d4afbf60
--- /dev/null
+++ b/ui/src/main/res/values-el-rGR/strings.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Δεν είναι δυνατή η διαγραφή του %d tunnel: %s</item>
+ <item quantity="other">Δεν είναι δυνατή η διαγραφή %d tunnels: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Το %d tunnel διαγράφηκε με επιτυχία</item>
+ <item quantity="other">Διαγράφηκαν επιτυχώς τα %d tunnels</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d επιλεγμένο tunnel</item>
+ <item quantity="other">%d επιλεγμένα tunnels</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Έγινε εισαγωγή %1$d από %2$d tunnels</item>
+ <item quantity="other">Έγινε εισαγωγή %1$d από %2$d tunnels</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Εισαγωγή tunnel %d</item>
+ <item quantity="other">Εισαγωγή tunnels %d</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d Εξαιρούμενη εφαρμογή</item>
+ <item quantity="other">%d Εξαιρούμενες εφαρμογές</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Συμπεριλαμβανόμενη εφαρμογή</item>
+ <item quantity="other">%d Συμπεριλαμβανόμενες εφαρμογές</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">εξαιρέθηκε %d</item>
+ <item quantity="other">εξαιρέθηκε %d</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">περιλαμβάνεται %d</item>
+ <item quantity="other">περιλαμβάνεται %d</item>
+ </plurals>
+ <string name="all_applications">Όλες οι εφαρμογές</string>
+ <string name="exclude_from_tunnel">Εξαίρεση</string>
+ <string name="include_in_tunnel">Συμπεριλάβετε μόνο</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Συμπερίληψη %d app</item>
+ <item quantity="other">Συμπερίληψη %d apps</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Εξαίρεση %d app</item>
+ <item quantity="other">Εξαίρεση %d apps</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">κάθε δευτερόλεπτο</item>
+ <item quantity="other">κάθε %d δευτερόλεπτα</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">δευτερόλεπτο</item>
+ <item quantity="other">δευτερόλεπτα</item>
+ </plurals>
+ <string name="use_all_applications">Χρησιμοποίησε όλες τις εφαρμογές</string>
+ <string name="add_peer">Προσθήκη peer</string>
+ <string name="addresses">Διευθύνσεις</string>
+ <string name="applications">Εφαρμογές</string>
+ <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
new file mode 100644
index 00000000..709b183b
--- /dev/null
+++ b/ui/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Imposible eliminar %d túnel: %s</item>
+ <item quantity="other">Imposible eliminar %d túnels: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Túnel eliminado correctamente %d</item>
+ <item quantity="other">Túneles eliminados correctamente %d</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d túnel seleccionado</item>
+ <item quantity="other">%d túneles seleccionados</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importados %1$d de %2$d túneles</item>
+ <item quantity="other">Importados %1$d de %2$d túneles</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d túnel importado</item>
+ <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>
+ </plurals>
+ <plurals name="set_included_applications">
+ <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>
+ <item quantity="other">%d excluidos</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d incluído</item>
+ <item quantity="other">%d incluidos</item>
+ </plurals>
+ <string name="all_applications">Todas las Aplicaciones</string>
+ <string name="exclude_from_tunnel">Excluir</string>
+ <string name="include_in_tunnel">Sólo inclusión</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Incluir %d aplicación</item>
+ <item quantity="other">Incluir %d aplicaciones</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Excluir %d aplicación</item>
+ <item quantity="other">Excluir %d aplicaciones</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">cada segundo</item>
+ <item quantity="other">cada %d segundos</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">segundo</item>
+ <item quantity="other">segundos</item>
+ </plurals>
+ <string name="use_all_applications">Usar todas las aplicaciones</string>
+ <string name="add_peer">Añadir par</string>
+ <string name="addresses">Direcciones</string>
+ <string name="applications">Aplicaciones</string>
+ <string name="allow_remote_control_intents_summary_off">Las aplicaciones externas no pueden cambiar 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>
+ <string name="bad_config_error">%1$s en %2$s</string>
+ <string name="bad_config_explanation_pka">: Debe ser positivo y no más de 65535</string>
+ <string name="bad_config_explanation_positive_number">: Debe ser positivo</string>
+ <string name="bad_config_explanation_udp_port">: Debe ser un número válido de puerto UDP</string>
+ <string name="bad_config_reason_invalid_key">Clave inválida</string>
+ <string name="bad_config_reason_invalid_number">Número inválido</string>
+ <string name="bad_config_reason_invalid_value">Valor inválido</string>
+ <string name="bad_config_reason_missing_attribute">Falta atributo</string>
+ <string name="bad_config_reason_missing_section">Sección faltante</string>
+ <string name="bad_config_reason_syntax_error">Error de sintaxis</string>
+ <string name="bad_config_reason_unknown_attribute">Atributo desconocido</string>
+ <string name="bad_config_reason_unknown_section">Sección desconocida</string>
+ <string name="bad_config_reason_value_out_of_range">Valor fuera de rango</string>
+ <string name="bad_extension_error">El archivo debe ser .conf o .zip</string>
+ <string name="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>
+ <string name="config_file_exists_error">El archivo de configuración “%s” ya existe</string>
+ <string name="config_not_found_error">Archivo de configuración “%s” no encontrado</string>
+ <string name="config_rename_error">No se puede renombrar el archivo de configuración “%s”</string>
+ <string name="config_save_error">No se puede guardar la configuración para “%1$s”: %2$s</string>
+ <string name="config_save_success">Configuración guardada correctamente para “%s”</string>
+ <string name="create_activity_title">Crear túnel WireGuard</string>
+ <string name="create_bin_dir_error">No se puede crear el directorio binario local</string>
+ <string name="create_downloads_file_error">No se puede crear archivo en el directorio de descargas</string>
+ <string name="create_empty">Crear de cero</string>
+ <string name="create_from_file">Importar desde archivo</string>
+ <string name="create_from_qr_code">Escanear desde código QR</string>
+ <string name="create_output_dir_error">No se puede crear el directorio de salida</string>
+ <string name="create_temp_dir_error">No se puede crear la carpeta temporal</string>
+ <string name="create_tunnel">Crear túnel</string>
+ <string name="copied_to_clipboard">%s copiado al portapapeles</string>
+ <string name="dark_theme_summary_off">Actualmente usando tema claro (día)</string>
+ <string name="dark_theme_summary_on">Actualmente usando tema oscuro (noche)</string>
+ <string name="dark_theme_title">Usar tema oscuro</string>
+ <string name="delete">Eliminar</string>
+ <string name="tv_delete">Seleccione el túnel para eliminar</string>
+ <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="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">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>
+ <string name="generic_error">Error desconocido “%s”</string>
+ <string name="hint_automatic">(auto)</string>
+ <string name="hint_generated">(generado)</string>
+ <string name="hint_optional">(opcional)</string>
+ <string name="hint_optional_discouraged">(opcional, no recomendado)</string>
+ <string name="hint_random">(aleatorio)</string>
+ <string name="illegal_filename_error">Nombre de archivo no válido “%s”</string>
+ <string name="import_error">No se puede importar túnel: %s</string>
+ <string name="import_from_qr_code">Importar túnel desde código QR</string>
+ <string name="import_success">Importado “%s”</string>
+ <string name="interface_title">Interfaz</string>
+ <string name="key_contents_error">Caracteres incorrectos en la clave</string>
+ <string name="key_length_error">Longitud de clave incorrecta</string>
+ <string name="key_length_explanation_base64">Las claves base64 de WireGuard deben tener 44 caracteres (32 bytes)</string>
+ <string name="key_length_explanation_binary">: Las claves WireGuard deben tener 32 bytes</string>
+ <string name="key_length_explanation_hex">: Las claves hexadecimales de Wirex deben tener 64 caracteres (32 bytes)</string>
+ <string name="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>
+ <string name="log_export_success">Guardado en “%s”</string>
+ <string name="log_export_title">Exportar archivo de registro</string>
+ <string name="log_saver_activity_label">Guardar registro</string>
+ <string name="log_viewer_pref_summary">Los registros pueden ayudar con la depuración</string>
+ <string name="log_viewer_pref_title">Ver registro de 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>
+ <string name="module_enabler_disabled_title">Habilitar backend del módulo del kernel</string>
+ <string name="module_enabler_enabled_summary">El backend más lento del espacio de usuario puede mejorar la estabilidad</string>
+ <string name="module_enabler_enabled_title">Desactivar backend del módulo del kernel</string>
+ <string name="module_installer_error">Ocurrió un error. Intente de nuevo</string>
+ <string name="module_installer_initial">El módulo experimental del kernel puede mejorar el rendimiento</string>
+ <string name="module_installer_not_found">No hay módulos disponibles para tu dispositivo</string>
+ <string name="module_installer_title">Descargar e instalar el módulo del kernel</string>
+ <string name="module_installer_working">Descargando e instalando…</string>
+ <string name="module_version_error">No se puede determinar la versión del módulo del kernel</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Activar un túnel apagará los demás</string>
+ <string name="multiple_tunnels_summary_on">Múltiples túneles pueden ser activados simultáneamente</string>
+ <string name="multiple_tunnels_title">Permitir múltiples túneles simultáneos</string>
+ <string name="name">Nombre</string>
+ <string name="no_config_error">Intentando abrir un túnel sin configuración</string>
+ <string name="no_configs_error">No se encontraron configuraciones</string>
+ <string name="no_tunnels_error">No existen túneles</string>
+ <string name="parse_error_generic">cadena</string>
+ <string name="parse_error_inet_address">Dirección IP</string>
+ <string name="parse_error_inet_endpoint">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">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>
+ <string name="public_key">Clave pública</string>
+ <string name="qr_code_hint">Consejo: generar con `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="restore_on_boot_summary_off">No mostrará túneles habilitados al arrancar</string>
+ <string name="restore_on_boot_summary_on">Mostrará túneles habilitados al arrancar</string>
+ <string name="restore_on_boot_title">Restaurar al arrancar</string>
+ <string name="save">Guardar</string>
+ <string name="select_all">Seleccionar todo</string>
+ <string name="settings">Preferencias</string>
+ <string name="shell_exit_status_read_error">Shell no puede leer 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>
+ <string name="toggle_all">Cambiar Todos</string>
+ <string name="toggle_error">Error al cambiar el túnel WireGuard: %s</string>
+ <string name="tools_installer_already">wg y wg-quick ya están instalados</string>
+ <string name="tools_installer_failure">No se puede instalar herramientas de línea de comandos (sin root?)</string>
+ <string name="tools_installer_initial">Instalar herramientas opcionales para el scripting</string>
+ <string name="tools_installer_initial_magisk">Instalar herramientas opcionales para el scripting como módulo Magisk</string>
+ <string name="tools_installer_initial_system">Instalar herramientas opcionales para el scripting en la partición del sistema</string>
+ <string name="tools_installer_success_magisk">wg y wg-quick instalados como un módulo Magisk (requiere reinicio)</string>
+ <string name="tools_installer_success_system">wg y wg-quick instalados en la partición del sistema</string>
+ <string name="tools_installer_title">Instalar herramientas de línea de comandos</string>
+ <string name="tools_installer_working">Instalando wg y wg-quick</string>
+ <string name="tools_unavailable_error">Herramientas requeridas no disponibles</string>
+ <string name="transfer">Transferir</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">No se puede crear el dispositivo túnel</string>
+ <string name="tunnel_config_error">No se puede configurar el túnel (wg-quick devuelto %d)</string>
+ <string name="tunnel_create_error">No se puede crear el dispositivo túnel: %s</string>
+ <string name="tunnel_create_success">Túnel creado con éxito “%s”</string>
+ <string name="tunnel_error_already_exists">Túnel “%s” ya existe</string>
+ <string name="tunnel_error_invalid_name">Nombre inválido</string>
+ <string name="tunnel_list_placeholder">Añadir un túnel usando el botón azul 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>
+ <string name="tunnel_rename_error">No se puede renombrar túnel: %s</string>
+ <string name="tunnel_rename_success">Túnel renombrado con éxito a “%s”</string>
+ <string name="type_name_go_userspace">Ir al espacio de usuario</string>
+ <string name="type_name_kernel_module">Módulo Kernel</string>
+ <string name="unknown_error">Error desconocido</string>
+ <string name="version_summary">%1$s backend %2$s</string>
+ <string name="version_summary_checking">Comprobando versión de backend %s</string>
+ <string name="version_summary_unknown">Versión %s desconocida</string>
+ <string name="version_title">WireGuard para Android -%s</string>
+ <string name="vpn_not_authorized_error">Servicio VPN no autorizado por el usuario</string>
+ <string name="vpn_start_error">No se puede iniciar el servicio VPN Android</string>
+ <string name="zip_export_error">No se pueden exportar túneles: %s</string>
+ <string name="zip_export_success">Guardado en “%s”</string>
+ <string name="zip_export_summary">El archivo Zip se guardará en la carpeta de descargas</string>
+ <string name="zip_export_title">Exportar túneles a archivo zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Autenticar para exportar túneles</string>
+ <string name="biometric_prompt_private_key_title">Autenticar para ver la clave privada</string>
+ <string name="biometric_auth_error">Error de autenticación</string>
+ <string name="biometric_auth_error_reason">Error de autenticación: %s</string>
+</resources>
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
new file mode 100644
index 00000000..e9000214
--- /dev/null
+++ b/ui/src/main/res/values-fa-rIR/strings.xml
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">حذف %d تونل‌ امکان‌پذیر نیست: %s</item>
+ <item quantity="other">حذف %d تونل‌ها امکان‌پذیر نیست: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d تونل با موقیت حذف شد</item>
+ <item quantity="other">%d تونل‌ با موفقیت حذف شد</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d تونل انتخاب شد</item>
+ <item quantity="other">%d تونل‌ها انتخاب شدند</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d از %2$d تونل اضافه شد</item>
+ <item quantity="other">%1$d از %2$d تونل اضافه شد</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d تونل اضافه شد</item>
+ <item quantity="other">%d از تونل اضافه شد</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d برنامه استثنا</item>
+ <item quantity="other">%d برنامه‌های استثنا</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d برنامه مشمول</item>
+ <item quantity="other">%d برنامه‌های مشمول</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d استثنا</item>
+ <item quantity="other">%d استثناها</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">شامل %d</item>
+ <item quantity="other">شامل %d</item>
+ </plurals>
+ <string name="all_applications">همه‌ برنامه‌ها</string>
+ <string name="exclude_from_tunnel">جدا کردن</string>
+ <string name="include_in_tunnel">تنها شامل</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">شامل %d برنامه</item>
+ <item quantity="other">شامل %d برنامه</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">جداکردن %d برنامه</item>
+ <item quantity="other">جداکردن %d برنامه</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">هر ثانیه</item>
+ <item quantity="other">هر %d ثانیه</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">ثانیه</item>
+ <item quantity="other">ثانیه</item>
+ </plurals>
+ <string name="use_all_applications">از همه برنامه‌ها استفاده کن</string>
+ <string name="add_peer">افزودن همتا</string>
+ <string name="addresses">نشانی‌ها</string>
+ <string name="applications">برنامه‌ها</string>
+ <string name="allow_remote_control_intents_summary_off">برنامه‌های بیرونی تونل ها را عوض نکنند
+(توصیه می‌شود)</string>
+ <string name="allow_remote_control_intents_summary_on">برنامه‌های بیرونی تونل‌ها را عوض کنند (پیشرفته)</string>
+ <string name="allow_remote_control_intents_title">اجازه به برنامه‌های کنترل از راه‌دور</string>
+ <string name="allowed_ips">IPهای مجاز</string>
+ <string name="bad_config_context">%1$s\'s %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s در %2$s</string>
+ <string name="bad_config_explanation_pka">: باید مثبت و بیشتر از ۶۵۵۳۵ نباشد</string>
+ <string name="bad_config_explanation_positive_number">: باید مثبت باشد</string>
+ <string name="bad_config_explanation_udp_port">: باید یک شماره پورت UDP معتبر باشد</string>
+ <string name="bad_config_reason_invalid_key">کلید نامعتبر است</string>
+ <string name="bad_config_reason_invalid_number">شماره نامعتبر است</string>
+ <string name="bad_config_reason_invalid_value">مقدار نامعتبر است</string>
+ <string name="bad_config_reason_missing_attribute">مشخصه موجود نیست</string>
+ <string name="bad_config_reason_missing_section">بخش موجود نیست</string>
+ <string name="bad_config_reason_syntax_error">خطای نحوی</string>
+ <string name="bad_config_reason_unknown_attribute">مشخصهٔ نامعلوم</string>
+ <string name="bad_config_reason_unknown_section">بخش نامعلوم</string>
+ <string name="bad_config_reason_value_out_of_range">مقدار خارج از محدوده</string>
+ <string name="bad_extension_error">پرونده باید .conf یا .zip باشد</string>
+ <string name="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>
+ <string name="config_file_exists_error">فایل پیکربندی ”%s” در حال حاضر وجود دارد</string>
+ <string name="config_not_found_error">پرونده پیکربندی “%s” یافت نشد</string>
+ <string name="config_rename_error">نمی‌توان نام پرونده پیکربندی “%s” را تغییر داد</string>
+ <string name="config_save_error">نمی‌توان پیکربندی برای “%1$s”: %2$s را ذخیره کرد</string>
+ <string name="config_save_success">پیکربندی برای “%s” با موفقیت ذخیره شد</string>
+ <string name="create_activity_title">ساخت تونل WireGuard</string>
+ <string name="create_bin_dir_error">نمی‌توان دایرکتوری باینری محلی را ایجاد کرد</string>
+ <string name="create_downloads_file_error">نمی‌توان در مسیر بارگیری پرونده‌ای ساخت</string>
+ <string name="create_empty">ساختن از ابتدا</string>
+ <string name="create_from_file">واردکردن از طریق پرونده یا آرشیو</string>
+ <string name="create_from_qr_code">اسکن از کد QR</string>
+ <string name="create_output_dir_error">نمی‌توان دایرکتوری خروجی را ایجاد کرد</string>
+ <string name="create_temp_dir_error">نمی‌توان دایرکتوری موقت محلی را ساخت</string>
+ <string name="create_tunnel">ساختن تونل</string>
+ <string name="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="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_up">خطا هنگام راه‌اندازی تونل: %s</string>
+ <string name="exclude_private_ips">مستثنی کردن IPهای خصوصی</string>
+ <string name="generate_new_private_key">تولید کلید خصوصی جدید</string>
+ <string name="generic_error">خطای “%s” ناشناخته</string>
+ <string name="hint_automatic">(خودکار)</string>
+ <string name="hint_generated">(تولید شده)</string>
+ <string name="hint_optional">(دلخواه)</string>
+ <string name="hint_optional_discouraged">(اختیاری، پیشنهاد نمی‌شود)</string>
+ <string name="hint_random">(تصادفی)</string>
+ <string name="illegal_filename_error">نام پرونده “%s” غیرمجاز است</string>
+ <string name="import_error">نمی‌توان تونل را وارد کرد: %s</string>
+ <string name="import_from_qr_code">وارد کردن تونل از کد QR</string>
+ <string name="import_success">“%s” وارد شد</string>
+ <string name="interface_title">رابط</string>
+ <string name="key_contents_error">در کلید نویسه‌های بد وجود دارد</string>
+ <string name="key_length_error">طول کلید نادرست است</string>
+ <string name="key_length_explanation_base64">: کلیدهای WireGuard base64 باید دارای ۴۴ نویسه باشند ( ۳۲ بایت)</string>
+ <string name="key_length_explanation_binary">: کلیدهای WireGuard باید ۳۲ بایت باشند</string>
+ <string name="key_length_explanation_hex">: کلیدهای هگز WireGuard باید دارای ۶۴ نویسه باشند ( ۳۲ بایت)</string>
+ <string name="listen_port">شنود پورت</string>
+ <string name="log_export_error">نمی‌توان گزارش رویداد را برون‌برد: %s</string>
+ <string name="log_export_subject">پرونده گزارش رویداد WireGuard اندروید</string>
+ <string name="log_export_success">ذخیره شد در “%s”</string>
+ <string name="log_export_title">برون‌برد پرونده گزارش رویداد</string>
+ <string name="log_saver_activity_label">ذخیره گزارش رویداد</string>
+ <string name="log_viewer_pref_summary">گزارش رویداد شاید به اشکال زدایی کمک کند</string>
+ <string name="log_viewer_pref_title">نمایش گزارش رویداد برنامه</string>
+ <string name="log_viewer_title">گزارش رویداد</string>
+ <string name="logcat_error">نمی‌توان logcat را اجرا کرد: </string>
+ <string name="module_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">نشانی IP</string>
+ <string name="parse_error_inet_endpoint">نقطه پایان</string>
+ <string name="parse_error_inet_network">شبکه IP</string>
+ <string name="parse_error_integer">شماره</string>
+ <string name="parse_error_reason">نمی‌توان %1$s “%2$s” تجزیه کرد</string>
+ <string name="peer">همتا</string>
+ <string name="permission_description">کنترل تونل های وایرگارد، فعال و غیرفعال کردن تونل ها، و حتی تغییر مسیر ترافیک اینترنت</string>
+ <string name="permission_label">کنترل تونل‌های WireGuard</string>
+ <string name="persistent_keepalive">زنده نگه‌داشتن پیوسته</string>
+ <string name="pre_shared_key">کلید از پیش تقسیم شده</string>
+ <string name="pre_shared_key_enabled">فعال شده</string>
+ <string name="private_key">کلید خصوصی</string>
+ <string name="public_key">کلید عمومی</string>
+ <string name="qr_code_hint">نکته: با `qrencode -t ansiutf8 &lt; tunnel.conf` تولید کنید.</string>
+ <string name="restore_on_boot_summary_off">تونل های فعال در لحظه بالا آمدن سیستم، روشن نخواهند شد</string>
+ <string name="restore_on_boot_summary_on">تونل های فعال در لحظه بالا آمدن سیستم، روشن خواهند شد</string>
+ <string name="restore_on_boot_title">بازگردانی در بوت</string>
+ <string name="save">ذخیره</string>
+ <string name="select_all">انتخاب همه</string>
+ <string name="settings">تنظیمات</string>
+ <string name="shell_exit_status_read_error">پوسته نمی تواند وضعیت خروجی را بخواند</string>
+ <string name="shell_marker_count_error">شل انتظار داشت 4 نشانگر دریافت شود %d</string>
+ <string name="shell_start_error">آغاز پوسته شکست خورد: %d</string>
+ <string name="success_application_will_restart">موفقیت. برنامه اکنون دوباره راه‌اندازی خواهد شد…</string>
+ <string name="toggle_all">معکوس کردن همه</string>
+ <string name="toggle_error">خطا در ضامن تونل WireGuard: %s</string>
+ <string name="tools_installer_already">wg و wg-quick قبلاً نصب شده اند</string>
+ <string name="tools_installer_failure">ابزارهای خط-فرمان نصب نمی‌شود (روت نیستید؟)</string>
+ <string name="tools_installer_initial">ابزارهای اختیاری را برای اسکریپت نویسی نصب کنید</string>
+ <string name="tools_installer_initial_magisk">ابزار اختیاری را برای اسکریپت نویسی به عنوان ماژول Magisk نصب کنید</string>
+ <string name="tools_installer_initial_system">ابزارهای اختیاری را برای اسکریپت نویسی در پارتیشن سیستم نصب کنید</string>
+ <string name="tools_installer_success_magisk">wg و wg-quick به عنوان پودمان Magisk نصب شدند
+( نیاز به راه‌اندازی دوباره)</string>
+ <string name="tools_installer_success_system">wg و wg-fast در پارتیشن سیستم نصب شده است</string>
+ <string name="tools_installer_title">ابزارهای خط فرمان را نصب کنید</string>
+ <string name="tools_installer_working">در حال نصب wg و wg-quick</string>
+ <string name="tools_unavailable_error">ابزارهای لازم در دسترس نیست</string>
+ <string name="transfer">انتقال</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">نمی‌توان تونل را ساخت</string>
+ <string name="tunnel_config_error">ناتوان در کانفیگ و ساخت تونل (wg-quick returned %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 برای اندروید نگارش %s</string>
+ <string name="vpn_not_authorized_error">کاربر به سرویس VPN اجازه نداد</string>
+ <string name="vpn_start_error">نمی‌توان سرویس VPN اندروید را آغاز کرد</string>
+ <string name="zip_export_error">نمی‌توان تونل‌ها را برون‎برد: %s</string>
+ <string name="zip_export_success">ذخیره شد در “%s”</string>
+ <string name="zip_export_summary">پرونده زیپ در پوشه بارگیری‌ها ذخیره خواهد شد</string>
+ <string name="zip_export_title">برون‌بری تونل‌ها در پرونده زیپ</string>
+ <string name="biometric_prompt_zip_exporter_title">برای برون‌بری تونل‌ها، هویت خود را تایید کنید</string>
+ <string name="biometric_prompt_private_key_title">برای دیدن کلید خصوصی، هویت خود را تایید کنید</string>
+ <string name="biometric_auth_error">شکست در تایید هویت</string>
+ <string name="biometric_auth_error_reason">شکست در تایید هویت: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-fi-rFI/strings.xml b/ui/src/main/res/values-fi-rFI/strings.xml
new file mode 100644
index 00000000..d1a714e5
--- /dev/null
+++ b/ui/src/main/res/values-fi-rFI/strings.xml
@@ -0,0 +1,90 @@
+<?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>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">sekunti</item>
+ <item quantity="other">sekuntia</item>
+ </plurals>
+ <string name="use_all_applications">Käytä kaikkia sovelluksia</string>
+ <string name="add_peer">Lisää toinen osapuoli</string>
+ <string name="addresses">Osoitteet</string>
+ <string name="applications">Sovellukset</string>
+ <string name="allowed_ips">Sallitut IP-osoitteet</string>
+ <string name="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>
+ <string name="bad_config_reason_missing_attribute">Attribuutti puuttuu</string>
+ <string name="bad_config_reason_missing_section">Osio puuttuu</string>
+ <string name="bad_config_reason_syntax_error">Syntaksivirhe</string>
+ <string name="bad_config_reason_unknown_attribute">Tuntematon määrite</string>
+ <string name="bad_config_reason_unknown_section">Tuntematon osio</string>
+ <string name="bad_config_reason_value_out_of_range">Arvo alueen ulkopuolella</string>
+ <string name="bad_extension_error">Tiedoston on oltava .conf tai .zip</string>
+ <string name="cancel">Peruuta</string>
+ <string name="config_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>
+ <string name="hint_automatic">(oletus)</string>
+ <string name="hint_generated">(generoitu)</string>
+ <string name="hint_optional">(valinnainen)</string>
+ <string name="hint_optional_discouraged">(valinnainen, ei suositeltava)</string>
+ <string name="hint_random">(satunnainen)</string>
+ <string name="illegal_filename_error">Virheellinen tiedostonimi “%s”</string>
+ <string name="import_error">Tunnelia \"%s\" ei voitu tuoda</string>
+ <string name="import_from_qr_code">Tuo tunneli QR-koodista</string>
+ <string name="import_success">Tuotu ”%s”</string>
+ <string name="key_contents_error">Virheellinen merkki avaimessa</string>
+ <string name="key_length_error">Virheellinen avaimen pituus</string>
+ <string name="key_length_explanation_base64">: WireGuardin base64-avainten pituus on oltava 44 merkkiä (32 tavua)</string>
+ <string name="key_length_explanation_binary">: WireGuard-avainten on oltava 32 tavua</string>
+ <string name="key_length_explanation_hex">: WireGuardin base64-avainten pituus on oltava 64 merkkiä (32 tavua)</string>
+ <string name="listen_port">Kuuntele porttia</string>
+ <string name="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>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tunnel_name">Tunnelin nimi</string>
+ <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
new file mode 100644
index 00000000..d4d4b95f
--- /dev/null
+++ b/ui/src/main/res/values-fr/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Impossible de supprimer %d tunnel : %s</item>
+ <item quantity="other">Impossible de supprimer %d tunnels : %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Suppression réussie du tunnel %d</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>
+ <item quantity="other">%d tunnels sélectionnés</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d de %2$d tunnels importés</item>
+ <item quantity="other">%1$d de %2$d tunnels importés</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d tunnel importé</item>
+ <item quantity="other">%d tunnels importés</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d application exclue</item>
+ <item quantity="other">%d applications exclues</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d application incluse</item>
+ <item quantity="other">%d applications incluses</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d exclue</item>
+ <item quantity="other">%d exclues</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d incluse</item>
+ <item quantity="other">%d incluses</item>
+ </plurals>
+ <string name="all_applications">Toutes les Applications</string>
+ <string name="exclude_from_tunnel">Exclure</string>
+ <string name="include_in_tunnel">Inclure uniquement</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Inclure %d application</item>
+ <item quantity="other">Inclure %d applications</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Exclure %d application</item>
+ <item quantity="other">Exclure %d applications</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">chaque seconde</item>
+ <item quantity="other">tous les %d secondes</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">seconde</item>
+ <item quantity="other">secondes</item>
+ </plurals>
+ <string name="use_all_applications">Utiliser toutes les applications</string>
+ <string name="add_peer">Ajouter un pair</string>
+ <string name="addresses">Adresses</string>
+ <string name="applications">Applications</string>
+ <string name="allow_remote_control_intents_summary_off">Les applications externes ne peuvent pas activer les tunnels (recommandé)</string>
+ <string name="allow_remote_control_intents_summary_on">Les applications externes peuvent activer les tunnels (avancés)</string>
+ <string name="allow_remote_control_intents_title">Autoriser les applications de contrôle à distance</string>
+ <string name="allowed_ips">Adresses IP autorisées</string>
+ <string name="bad_config_context">%1$s de %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s dans %2$s</string>
+ <string name="bad_config_explanation_pka">: Doit être positif et ne pas dépasser 65535</string>
+ <string name="bad_config_explanation_positive_number">: Doit être positif</string>
+ <string name="bad_config_explanation_udp_port">: Doit être un numéro de port UDP valide</string>
+ <string name="bad_config_reason_invalid_key">Clef invalide</string>
+ <string name="bad_config_reason_invalid_number">Numéro invalide</string>
+ <string name="bad_config_reason_invalid_value">Valeur invalide</string>
+ <string name="bad_config_reason_missing_attribute">Attribut manquant</string>
+ <string name="bad_config_reason_missing_section">Section manquante</string>
+ <string name="bad_config_reason_syntax_error">Erreur de syntaxe</string>
+ <string name="bad_config_reason_unknown_attribute">Attribut inconnu</string>
+ <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="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">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>
+ <string name="copied_to_clipboard">%s copié dans le presse-papier</string>
+ <string name="dark_theme_summary_off">Utilisation en cours du thème clair (jour)</string>
+ <string name="dark_theme_summary_on">Utilisation en cours du thème sombre (nuit)</string>
+ <string name="dark_theme_title">Utilisez le thème sombre</string>
+ <string name="delete">Supprimer</string>
+ <string name="tv_delete">Sélectionner le tunnel à supprimer</string>
+ <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>
+ <string name="generic_error">Erreur inconnue « %s »</string>
+ <string name="hint_automatic">(auto)</string>
+ <string name="hint_generated">(généré)</string>
+ <string name="hint_optional">(facultatif)</string>
+ <string name="hint_optional_discouraged">(facultatif, non recommandé)</string>
+ <string name="hint_random">(aléatoire)</string>
+ <string name="illegal_filename_error">Nom de fichier invalide “%s”</string>
+ <string name="import_error">Impossible d\'importer le tunnel : %s</string>
+ <string name="import_from_qr_code">Importer un tunnel depuis un QR-code</string>
+ <string name="import_success">Importé «%s »</string>
+ <string name="interface_title">Interface</string>
+ <string name="key_contents_error">Mauvais caractères dans la clé</string>
+ <string name="key_length_error">Longueur de la clé incorrecte</string>
+ <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>
+ <string name="log_export_success">Enregistré dans « %s »</string>
+ <string name="log_export_title">Exporter le fichier journal</string>
+ <string name="log_saver_activity_label">Enregistrer le journal</string>
+ <string name="log_viewer_pref_summary">Les journaux peuvent aider au débogage</string>
+ <string name="log_viewer_pref_title">Afficher le journal de l\'application</string>
+ <string name="log_viewer_title">Journal</string>
+ <string name="logcat_error">Impossible d\'exécuter logcat : </string>
+ <string name="module_enabler_disabled_summary">Le module expérimental du noyau peut améliorer les performances</string>
+ <string name="module_enabler_disabled_title">Activer le backend du module du noyau</string>
+ <string name="module_enabler_enabled_summary">Le backend plus lent de l\'espace utilisateur peut améliorer la stabilité</string>
+ <string name="module_enabler_enabled_title">Désactiver le backend du module du noyau</string>
+ <string name="module_installer_error">Une erreur est survenue. Veuillez réessayer</string>
+ <string name="module_installer_initial">Le module expérimental du noyau peut améliorer les performances</string>
+ <string name="module_installer_not_found">Aucun module n\'est disponible pour votre appareil</string>
+ <string name="module_installer_title">Télécharger et installer le module du noyau</string>
+ <string name="module_installer_working">Téléchargement et installation en cours…</string>
+ <string name="module_version_error">Impossible de déterminer la version du module noyau</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Activer un tunnel éteindra les autres</string>
+ <string name="multiple_tunnels_summary_on">Plusieurs tunnels peuvent être activés simultanément</string>
+ <string name="multiple_tunnels_title">Autoriser plusieurs tunnels simultanés</string>
+ <string name="name">Nom</string>
+ <string name="no_config_error">Tentative d\'établir un tunnel sans configuration</string>
+ <string name="no_configs_error">Aucune configuration trouvée</string>
+ <string name="no_tunnels_error">Aucun tunnel existant</string>
+ <string name="parse_error_generic">chaîne de caractères</string>
+ <string name="parse_error_inet_address">Adresse IP</string>
+ <string name="parse_error_inet_endpoint">point de terminaison</string>
+ <string name="parse_error_inet_network">Réseau IP</string>
+ <string name="parse_error_integer">nombre</string>
+ <string name="parse_error_reason">Impossible d\'analyser %1$s “%2$s”</string>
+ <string name="peer">Pair</string>
+ <string name="permission_description">contrôler les tunnels WireGuard, activer et désactiver les tunnels à volonté, potentiellement détourner le trafic Internet</string>
+ <string name="permission_label">contrôler les tunnels WireGuard</string>
+ <string name="persistent_keepalive">Maintien de connexion permanente</string>
+ <string name="pre_shared_key">Clé pré-partagée</string>
+ <string name="pre_shared_key_enabled">activée</string>
+ <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>
+ <string name="save">Enregistrer</string>
+ <string name="select_all">Tout sélectionner</string>
+ <string name="settings">Paramètres</string>
+ <string name="shell_exit_status_read_error">L\'interpréteur de commande ne peut pas lire l\'état de sortie</string>
+ <string name="shell_marker_count_error">L\'interpréteur de commandes attendait 4 marqueurs, %d reçus</string>
+ <string name="shell_start_error">L\'interpréteur de commande n\'a pas pu démarrer : %d</string>
+ <string name="success_application_will_restart">Succès. L\'application va redémarrer maintenant…</string>
+ <string name="toggle_all">Tout basculer</string>
+ <string name="toggle_error">Erreur lors de l\'activation du tunnel WireGuard : %s</string>
+ <string name="tools_installer_already">wg et wg-quick sont déjà installés</string>
+ <string name="tools_installer_failure">Impossible d\'installer les outils en ligne de commande (pas de root?)</string>
+ <string name="tools_installer_initial">Installer les outils optionnels pour le script</string>
+ <string name="tools_installer_initial_magisk">Installer les outils optionnels pour le script en tant que module Magisk</string>
+ <string name="tools_installer_initial_system">Installer les outils optionnels pour le script dans la partition système</string>
+ <string name="tools_installer_success_magisk">wg et wg-quick installés en tant que module Magisk (redémarrage requis)</string>
+ <string name="tools_installer_success_system">wg et wg-quick installés dans la partition système</string>
+ <string name="tools_installer_title">Installer les outils de ligne de commande</string>
+ <string name="tools_installer_working">Installation de wg et wg-quick</string>
+ <string name="tools_unavailable_error">Outils requis indisponibles</string>
+ <string name="transfer">Données transférées</string>
+ <string name="transfer_bytes">%d Octets</string>
+ <string name="transfer_gibibytes">%.2f Go</string>
+ <string name="transfer_kibibytes">%.2f Ko</string>
+ <string name="transfer_mibibytes">%.2f Mo</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f To</string>
+ <string name="tun_create_error">Impossible de créer le périphérique tun</string>
+ <string name="tunnel_config_error">Impossible de configurer le tunnel (wg-quick a retourné %d)</string>
+ <string name="tunnel_create_error">Impossible de créer le tunnel : %s</string>
+ <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">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">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>
+ <string name="version_title">WireGuard pour Android v%s</string>
+ <string name="vpn_not_authorized_error">Service VPN non autorisé par l\'utilisateur</string>
+ <string name="vpn_start_error">Impossible de démarrer le service VPN Android</string>
+ <string name="zip_export_error">Impossible d\'exporter les tunnels : %s</string>
+ <string name="zip_export_success">Enregistré dans « %s »</string>
+ <string name="zip_export_summary">L\'archive zip sera sauvegardée dans le dossier de téléchargement</string>
+ <string name="zip_export_title">Exporter les tunnels vers le fichier zip</string>
+ <string name="biometric_prompt_zip_exporter_title">S\'authentifier pour exporter des tunnels</string>
+ <string name="biometric_prompt_private_key_title">S\'authentifier pour voir la clé privée</string>
+ <string name="biometric_auth_error">Échec de l\'authentification</string>
+ <string name="biometric_auth_error_reason">Échec de l\'authentification : %s</string>
+</resources>
diff --git a/ui/src/main/res/values-hi-rIN/strings.xml b/ui/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 00000000..737f83a8
--- /dev/null
+++ b/ui/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d टनल हटाने में असमर्थ: %s</item>
+ <item quantity="other">%d टनलस को हटाने में असमर्थ: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d टनल को सफलतापूर्वक हटा दिया गया</item>
+ <item quantity="other">%d टनलस को सफलतापूर्वक हटा दिया गया</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d टनल चयनित</item>
+ <item quantity="other">%d टनलस का चयन किया गया</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">आयातित %d %d टनल</item>
+ <item quantity="other">आयातित %d %d टनलस</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">आयातित %d टनल</item>
+ <item quantity="other">आयातित %d टनलस</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d बहिष्कृत अनुप्रयोग</item>
+ <item quantity="other">%d बहिष्कृत अनुप्रयोग</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d ऐप्स शामिल</item>
+ <item quantity="other">%d ऐप्स शामिल किये गए</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d अपवर्जित</item>
+ <item quantity="other">%d अपवर्जित</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d शामिल</item>
+ <item quantity="other">%d शामिल</item>
+ </plurals>
+ <string name="all_applications">सभी एप्लीकेशन</string>
+ <string name="exclude_from_tunnel">वर्जित</string>
+ <string name="include_in_tunnel">केवल शामिल करें</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">%d ऐप शामिल करें</item>
+ <item quantity="other">%d ऐप्स शामिल करें</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">%d ऐप को बाहर करें</item>
+ <item quantity="other">%d ऐप्स को बाहर करें</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">हर सेकंड</item>
+ <item quantity="other">हर %d सेकंड्‌स</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">सेकंड</item>
+ <item quantity="other">सेकंड्‌स</item>
+ </plurals>
+ <string name="use_all_applications">सभी ऐप्स का उपयोग करें</string>
+ <string name="add_peer">पीयर जोड़ें</string>
+ <string name="addresses">एड्रेससैस</string>
+ <string name="applications">ऍप्लिकेशन्स</string>
+ <string name="allow_remote_control_intents_summary_off">बाहरी ऐप्स टनल्स को चालू नहीं कर सकते (अनुशंसित)</string>
+ <string name="allow_remote_control_intents_summary_on">बाहरी ऐप्स टनल्स को चालू कर सकते है (एडवांस्ड)</string>
+ <string name="allow_remote_control_intents_title">रिमोट कंट्रोल ऐप्स की अनुमति दें</string>
+ <string name="allowed_ips">अनुमत आईपी</string>
+ <string name="bad_config_explanation_pka">: सकारात्मक होना चाहिए और 65535 से अधिक नहीं होना चाहिए</string>
+ <string name="bad_config_explanation_positive_number">: सकारात्मक होना चाहिए</string>
+ <string name="bad_config_explanation_udp_port">: एक वैध यूडीपी पोर्ट नंबर होना चाहिए</string>
+ <string name="bad_config_reason_invalid_key">अमान्य चाबी</string>
+ <string name="bad_config_reason_invalid_number">अमान्य संख्या</string>
+ <string name="bad_config_reason_invalid_value">अमान्य मूल्य</string>
+ <string name="bad_config_reason_missing_attribute">गुम विशेषता</string>
+ <string name="bad_config_reason_missing_section">छूटा हुआ भाग</string>
+ <string name="bad_config_reason_syntax_error">वक्य रचना त्रुटि</string>
+ <string name="bad_config_reason_unknown_attribute">अज्ञात एट्रिब्यूट</string>
+ <string name="bad_config_reason_unknown_section">अज्ञात एट्रिब्यूट </string>
+ <string name="bad_config_reason_value_out_of_range">मूल्य सीमा से बाहर</string>
+ <string name="bad_extension_error">फ़ाइल .conf या .zip होनी चाहिए</string>
+ <string name="error_no_qr_found">छवि में क्यूआर कोड नहीं मिला</string>
+ <string name="cancel">रद्द</string>
+ <string name="config_delete_error">कॉन्फ़िगरेशन फ़ाइल %s को नहीं हटा सकता</string>
+ <string name="config_exists_error">“%s” के लिए कॉन्फ़िगरेशन पहले से मौजूद है</string>
+ <string name="config_file_exists_error">कॉन्फ़िगरेशन फ़ाइल “%s” पहले से मौजूद है</string>
+ <string name="config_not_found_error">कॉन्फ़िगरेशन फ़ाइल “%s” नहीं मिली</string>
+ <string name="config_rename_error">कॉन्फ़िगरेशन फ़ाइल “%s” का नाम नहीं बदल सकता</string>
+ <string name="config_save_error">“%1$s” के लिए कॉन्फ़िगरेशन को नहीं बचा सकता: %2$s</string>
+ <string name="config_save_success">“%s” के लिए सफलतापूर्वक सहेजा गया कॉन्फ़िगरेशन</string>
+ <string name="create_activity_title">वायरगार्ड टनल बनाएं</string>
+ <string name="create_bin_dir_error">स्थानीय बाइनरी निर्देशिका नहीं बना सकते</string>
+ <string name="create_downloads_file_error">डाउनलोड निर्देशिका में फ़ाइल नहीं बना सकते</string>
+ <string name="create_empty">शुरू से बनाएँ</string>
+ <string name="create_from_file">फ़ाइल या संग्रह से आयात करें</string>
+ <string name="create_from_qr_code">QR कोड स्कैन करें</string>
+ <string name="create_output_dir_error">आउटपुट निर्देशिका नहीं बना सकता</string>
+ <string name="create_temp_dir_error">स्थानीय अस्थायी निर्देशिका नहीं बना सकते</string>
+ <string name="create_tunnel">टनल बनाए</string>
+ <string name="dark_theme_summary_off">अभी प्रकाश (दिन) थीम का उपयोग कर रहे हैं</string>
+ <string name="dark_theme_summary_on">अभी डार्क (रात) थीम का उपयोग कर रहे हैं</string>
+ <string name="dark_theme_title">डार्क थीम का इस्तेमाल करें</string>
+ <string name="delete">हटाएं</string>
+ <string name="dns_servers">डीएनएस सर्वर</string>
+ <string name="edit">संपादित करें</string>
+ <string name="endpoint">अंतिम</string>
+ <string name="error_down">टनल को लाने में त्रुटि: %s</string>
+ <string name="error_fetching_apps">ऐप्स सूची लाने में त्रुटि: %s</string>
+ <string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
+ <string name="error_up">टनल को लाने में त्रुटि: %s</string>
+ <string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
+ <string name="generate_new_private_key">नई प्राइवेट की उत्पन्न करें</string>
+ <string name="generic_error">अज्ञात “%s” त्रुटि</string>
+ <string name="hint_automatic">(ऑटो)</string>
+ <string name="hint_generated">(उत्पन्न)</string>
+ <string name="hint_optional">(ऐच्छिक)</string>
+ <string name="hint_optional_discouraged">(वैकल्पिक, अनुशंसित नहीं)</string>
+ <string name="hint_random">(क्रमरहित)</string>
+ <string name="illegal_filename_error">अवैध फ़ाइल नाम “%s”</string>
+ <string name="import_error">टनल को आयात करने में असमर्थ: %s</string>
+ <string name="import_from_qr_code">क्यूआर कोड से टनल को आयात करें</string>
+ <string name="import_success">आयातित “%s”</string>
+ <string name="interface_title">इंटरफेस</string>
+ <string name="key_contents_error">चाबी में खराब वर्ण</string>
+ <string name="key_length_error">चाबी की लम्बाई गलत </string>
+ <string name="key_length_explanation_base64">: वायरगार्ड बेस 64 कीज़ में 44 अक्षर (32 बाइट्स) होने चाहिए</string>
+ <string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
+ <string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
+ <string name="listen_port">पोर्ट सूने</string>
+ <string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string>
+ <string name="log_export_subject">WireGuard एंड्राइड लॉग फ़ाइल</string>
+ <string name="log_export_success">“%s” में सहेजा गया</string>
+ <string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
+ <string name="log_saver_activity_label">लॉग सहेजे</string>
+ <string name="log_viewer_pref_summary">लॉग डीबगिंग में सहायता कर सकते हैं</string>
+ <string name="log_viewer_pref_title">एप्लिकेशन लॉग देखें</string>
+ <string name="log_viewer_title">लॉग</string>
+ <string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
+ <string name="module_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="parse_error_reason">%1$s “%2$s” को पार्स नहीं कर सकता</string>
+ <string name="peer">पीयर</string>
+ <string name="permission_description">वायरगार्ड टनल्स को नियंत्रित करना, टनल्स को सक्षम और अक्षम करना, संभवतः इंटरनेट ट्रैफ़िक को गलत तरीके से अक्षम करना है</string>
+ <string name="permission_label">वायरगार्ड टनलस को नियंत्रित करें</string>
+ <string name="persistent_keepalive">लगातार जिंदा रहो</string>
+ <string name="pre_shared_key">प्री-शेयर्ड कीस</string>
+ <string name="pre_shared_key_enabled">सक्षम</string>
+ <string name="private_key">निजी कीस</string>
+ <string name="public_key">सार्वजनिक कीस</string>
+ <string name="qr_code_hint">सुझाव: `qrencode -t ansiutf8 &lt; tunnel.conf` के साथ उत्पन्न करो</string>
+ <string name="restore_on_boot_summary_off">बूट पर सक्षम टनलस को नहीं लाएगा</string>
+ <string name="restore_on_boot_summary_on">बूट पर सक्षम टनलस को लाएगा</string>
+ <string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
+ <string name="save">सहेजें</string>
+ <string name="select_all">सभी का चयन करे</string>
+ <string name="settings">सेटिंग्स</string>
+ <string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
+ <string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
+ <string name="shell_start_error">शेल शुरू करने में विफल: %d</string>
+ <string name="success_application_will_restart">सफलता। एप्लीकेशन अब पुनः आरंभ होगा…</string>
+ <string name="toggle_all">सबको स्विच करे</string>
+ <string name="toggle_error">वायरगार्ड टनल टॉगल करने में त्रुटि: %s</string>
+ <string name="tools_installer_already">wg और wg-quick पहले से इंस्टॉल हैं</string>
+ <string name="tools_installer_failure">कमांड-लाइन टूल स्थापित करने में असमर्थ (कोई रूट नहीं)</string>
+ <string name="tools_installer_initial">स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
+ <string name="tools_installer_initial_magisk">Magisk मॉड्यूल के रूप में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
+ <string name="tools_installer_initial_system">सिस्टम विभाजन में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
+ <string name="tools_installer_success_magisk">wg और wg-quick को मैजिक मॉड्यूल के रूप में स्थापित किया गया है (रिबूट आवश्यक)</string>
+ <string name="tools_installer_success_system">wg और wg-quick सिस्टम विभाजन में स्थापित है</string>
+ <string name="tools_installer_title">कमांड लाइन उपकरण स्थापित करें</string>
+ <string name="tools_installer_working">Wg और wg-quick इंस्टॉल करना</string>
+ <string name="tools_unavailable_error">आवश्यक उपकरण अनुपलब्ध हैं</string>
+ <string name="transfer">स्थानांतरण</string>
+ <string name="tun_create_error">ट्यून डिवाइस बनाने में असमर्थ</string>
+ <string name="tunnel_config_error">टनल को कॉन्फ़िगर करने में असमर्थ (wg-quick लौटा %d)</string>
+ <string name="tunnel_create_error">टनल बनाने में असमर्थ: %s</string>
+ <string name="tunnel_create_success">सफलतापूर्वक बनाया गया टनल “%s”</string>
+ <string name="tunnel_error_already_exists">टनल “%s” पहले से मौजूद है</string>
+ <string name="tunnel_error_invalid_name">गलत नाम</string>
+ <string name="tunnel_name">टनल का नाम</string>
+ <string name="tunnel_on_error">टनल चालू करने में असमर्थ (wgTurnOn लौटा %d)</string>
+ <string name="tunnel_rename_error">टनल का नाम बदलने में असमर्थ: %s</string>
+ <string name="tunnel_rename_success">सफलतापूर्वक टनल का नाम बदलकर “%s” कर दिया गया</string>
+ <string name="type_name_go_userspace">userspace पे जाए </string>
+ <string name="type_name_kernel_module">कर्नेल मॉड्यूल</string>
+ <string name="unknown_error">अज्ञात त्रुटि</string>
+ <string name="version_summary">%1$s बैकएंड %2$s</string>
+ <string name="version_summary_checking">%s बैकएंड संस्करण की जाँच कर रहा है</string>
+ <string name="version_summary_unknown">अज्ञात %s संस्करण</string>
+ <string name="version_title">WireGuard for Android v%s</string>
+ <string name="vpn_not_authorized_error">वीपीएन सेवा उपयोगकर्ता द्वारा अधिकृत नहीं है</string>
+ <string name="vpn_start_error">एंड्रॉयड वीपीएन सेवा प्रारंभ करने में असमर्थ</string>
+ <string name="zip_export_error">टनल का निर्यात करने में असमर्थ: %s</string>
+ <string name="zip_export_success">“%s” पर सहेजा गया</string>
+ <string name="zip_export_summary">ज़िप फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string>
+ <string name="zip_export_title">जिप फाइल के लिए टनल को एक्सपोर्ट करें</string>
+ <string name="biometric_prompt_zip_exporter_title">टनल्स के निर्यात के लिए प्रमाणित करें</string>
+ <string name="biometric_prompt_private_key_title">प्राइवेट की देखने के लिए प्रमाणित करें</string>
+ <string name="biometric_auth_error">प्रमाणीकरण विफलता</string>
+ <string name="biometric_auth_error_reason">प्रमाणीकरण विफल: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-hi/strings.xml b/ui/src/main/res/values-hi/strings.xml
new file mode 100644
index 00000000..1566d7a0
--- /dev/null
+++ b/ui/src/main/res/values-hi/strings.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d टनल हटाने में असमर्थ: %s</item>
+ <item quantity="other">%d टनलस को हटाने में असमर्थ: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d टनल को सफलतापूर्वक हटा दिया गया</item>
+ <item quantity="other">%d टनलस को सफलतापूर्वक हटा दिया गया</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d टनल चयनित</item>
+ <item quantity="other">%d टनलस का चयन किया गया</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">आयातित %d %d टनल</item>
+ <item quantity="other">आयातित %d %d टनलस</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">आयातित %d टनल</item>
+ <item quantity="other">आयातित %d टनलस</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d बहिष्कृत अनुप्रयोग</item>
+ <item quantity="other">%d बहिष्कृत अनुप्रयोग</item>
+ </plurals>
+ <string name="add_peer">पीयर जोड़ें</string>
+ <string name="addresses">एड्रेससैस</string>
+ <string name="allowed_ips">अनुमत आईपी</string>
+ <string name="app_name">WireGuard</string>
+ <string name="bad_config_context">%1$s\'s %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s in %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">: एक वैध यूडीपी पोर्ट नंबर होना चाहिए</string>
+ <string name="bad_config_reason_invalid_key">अमान्य चाबी</string>
+ <string name="bad_config_reason_invalid_number">अमान्य संख्या</string>
+ <string name="bad_config_reason_invalid_value">अमान्य मूल्य</string>
+ <string name="bad_config_reason_missing_attribute">गुम विशेषता</string>
+ <string name="bad_config_reason_missing_section">छूटा हुआ भाग</string>
+ <string name="bad_config_reason_syntax_error">वक्य रचना त्रुटि</string>
+ <string name="bad_config_reason_unknown_attribute">अज्ञात एट्रिब्यूट</string>
+ <string name="bad_config_reason_unknown_section">अज्ञात एट्रिब्यूट </string>
+ <string name="bad_config_reason_value_out_of_range">मूल्य सीमा से बाहर</string>
+ <string name="bad_extension_error">फ़ाइल .conf या .zip होनी चाहिए</string>
+ <string name="cancel">रद्द</string>
+ <string name="config_delete_error">कॉन्फ़िगरेशन फ़ाइल %s को नहीं हटा सकता</string>
+ <string name="config_exists_error">“%s” के लिए कॉन्फ़िगरेशन पहले से मौजूद है</string>
+ <string name="config_file_exists_error">कॉन्फ़िगरेशन फ़ाइल “%s” पहले से मौजूद है</string>
+ <string name="config_not_found_error">कॉन्फ़िगरेशन फ़ाइल “%s” नहीं मिली</string>
+ <string name="config_rename_error">कॉन्फ़िगरेशन फ़ाइल “%s” का नाम नहीं बदल सकता</string>
+ <string name="config_save_error">“%1$s” के लिए कॉन्फ़िगरेशन को नहीं बचा सकता: %2$s</string>
+ <string name="config_save_success">“%s” के लिए सफलतापूर्वक सहेजा गया कॉन्फ़िगरेशन</string>
+ <string name="create_activity_title">वायरगार्ड टनल बनाएं</string>
+ <string name="create_bin_dir_error">स्थानीय बाइनरी निर्देशिका नहीं बना सकते</string>
+ <string name="create_empty">शुरू से बनाएँ</string>
+ <string name="create_output_dir_error">आउटपुट निर्देशिका नहीं बना सकता</string>
+ <string name="create_downloads_file_error">डाउनलोड निर्देशिका में फ़ाइल नहीं बना सकते</string>
+ <string name="create_temp_dir_error">स्थानीय अस्थायी निर्देशिका नहीं बना सकते</string>
+ <string name="create_tunnel">टनल बनाए</string>
+ <string name="dark_theme_summary_off">अभी प्रकाश (दिन) थीम का उपयोग कर रहे हैं</string>
+ <string name="dark_theme_summary_on">अभी डार्क (रात) थीम का उपयोग कर रहे हैं</string>
+ <string name="dark_theme_title">डार्क थीम का इस्तेमाल करें</string>
+ <string name="delete">हटाएं</string>
+ <string name="toggle_all">सबको स्विच करे</string>
+ <string name="dns_servers">डीएनएस सर्वर</string>
+ <string name="edit">संपादित करें</string>
+ <string name="endpoint">अंतिम</string>
+ <string name="error_down">टनल को लाने में त्रुटि: %s</string>
+ <string name="error_fetching_apps">ऐप्स सूची लाने में त्रुटि: %s</string>
+ <string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
+ <string name="error_up">टनल को लाने में त्रुटि: %s</string>
+ <string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
+ <string name="generic_error">अज्ञात “%s” त्रुटि</string>
+ <string name="hint_automatic">(ऑटो)</string>
+ <string name="hint_generated">(उत्पन्न)</string>
+ <string name="hint_optional">(ऐच्छिक)</string>
+ <string name="hint_random">(क्रमरहित)</string>
+ <string name="illegal_filename_error">अवैध फ़ाइल नाम “%s”</string>
+ <string name="import_error">टनल को आयात करने में असमर्थ: %s</string>
+ <string name="import_from_qr_code">क्यूआर कोड से टनल को आयात करें</string>
+ <string name="import_success">आयातित “%s”</string>
+ <string name="interface_title">इंटरफेस</string>
+ <string name="key_length_explanation_base64">: वायरगार्ड बेस 64 कीज़ में 44 अक्षर (32 बाइट्स) होने चाहिए</string>
+ <string name="key_length_explanation_binary">: वायरगार्ड कीज 32 बाइट होनी चाहिए</string>
+ <string name="key_length_explanation_hex">: वायरगार्ड हेक्स कीज़ 64 अक्षरों की होनी चाहिए (32 बाइट्स)</string>
+ <string name="listen_port">पोर्ट सूने</string>
+ <string name="log_export_error">लॉग निर्यात करने में असमर्थ: %s</string>
+ <string name="log_export_success">“%s” में सहेजा गया</string>
+ <string name="log_export_title">लॉग फ़ाइल निर्यात करें</string>
+ <string name="logcat_error">लॉगकैट चलाने में असमर्थ: </string>
+ <string name="module_version_error">कर्नेल मॉड्यूल संस्करण निर्धारित करने में असमर्थ</string>
+ <string name="module_installer_not_found">आपके डिवाइस के लिए कोई मॉड्यूल उपलब्ध नहीं हैं</string>
+ <string name="module_installer_initial">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
+ <string name="module_installer_title">कर्नेल मॉड्यूल डाउनलोड और इंस्टॉल करें</string>
+ <string name="module_installer_working">डाउनलोड कर रहा है और स्थापित कर रहा है…</string>
+ <string name="module_installer_error">कुछ गलत हो गया। कृपया पुन: प्रयास करें</string>
+ <string name="module_enabler_disabled_title">कर्नेल मॉड्यूल बैकएंड सक्षम करें</string>
+ <string name="module_enabler_disabled_summary">प्रयोगात्मक कर्नेल मॉड्यूल प्रदर्शन में सुधार कर सकता है</string>
+ <string name="module_enabler_enabled_title">कर्नेल मॉड्यूल बैकएंड को अक्षम करें</string>
+ <string name="module_enabler_enabled_summary">धीमे यूजरस्पेस बैकएंड में स्थिरता में सुधार हो सकता है</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_title">एक साथ कई टनलस को अनुमति दें</string>
+ <string name="multiple_tunnels_summary_on">एक साथ कई टनलस को चालू किया जा सकता है</string>
+ <string name="multiple_tunnels_summary_off">एक टनल को चालू करने से अन्य बंद हो जाएंगे</string>
+ <string name="name">नाम</string>
+ <string name="no_config_error">बिना किसी कॉन्फ़िगरेशन के एक टनल को लाने की कोशिश करना</string>
+ <string name="no_configs_error">कोई कॉन्फ़िगरेशन नहीं मिला</string>
+ <string name="no_tunnels_error">कोई टनल मौजूद नहीं है</string>
+ <string name="parse_error_generic">पाठ</string>
+ <string name="parse_error_inet_address">आईपी पता</string>
+ <string name="parse_error_inet_endpoint">समाप्त</string>
+ <string name="parse_error_inet_network">आईपी नेटवर्क</string>
+ <string name="parse_error_integer">संख्या</string>
+ <string name="parse_error_reason">%1$s “%2$s” को पार्स नहीं कर सकता</string>
+ <string name="peer">पीयर</string>
+ <string name="permission_label">वायरगार्ड टनलस को नियंत्रित करें</string>
+ <string name="persistent_keepalive">लगातार जिंदा रहो</string>
+ <string name="pre_shared_key">प्री-शेयर्ड कीस</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_on">बूट पर सक्षम टनलस को लाएगा</string>
+ <string name="restore_on_boot_summary_off">बूट पर सक्षम टनलस को नहीं लाएगा</string>
+ <string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
+ <string name="save">सहेजें</string>
+ <string name="select_all">सभी का चयन करे</string>
+ <string name="settings">सेटिंग्स</string>
+ <string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
+ <string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
+ <string name="shell_start_error">शेल शुरू करने में विफल: %d</string>
+ <string name="toggle_error">वायरगार्ड टनल टॉगल करने में त्रुटि: %s</string>
+ <string name="tools_installer_already">wg और wg-quick पहले से इंस्टॉल हैं</string>
+ <string name="tools_installer_failure">कमांड-लाइन टूल स्थापित करने में असमर्थ (कोई रूट नहीं)</string>
+ <string name="tools_installer_initial">स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
+ <string name="tools_installer_initial_magisk">Magisk मॉड्यूल के रूप में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
+ <string name="tools_installer_initial_system">सिस्टम विभाजन में स्क्रिप्टिंग के लिए वैकल्पिक उपकरण स्थापित करें</string>
+ <string name="tools_installer_success_magisk">wg और wg-quick को मैजिक मॉड्यूल के रूप में स्थापित किया गया है (रिबूट आवश्यक)</string>
+ <string name="tools_installer_success_system">wg और wg-quick सिस्टम विभाजन में स्थापित है</string>
+ <string name="tools_installer_title">कमांड लाइन उपकरण स्थापित करें</string>
+ <string name="tools_installer_working">Wg और wg-quick इंस्टॉल करना</string>
+ <string name="tools_unavailable_error">आवश्यक उपकरण अनुपलब्ध हैं</string>
+ <string name="transfer">स्थानांतरण</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_gibibytes">%.2f GiB</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_rename_error">टनल का नाम बदलने में असमर्थ: %s</string>
+ <string name="tunnel_rename_success">सफलतापूर्वक टनल का नाम बदलकर “%s” कर दिया गया</string>
+ <string name="type_name_go_userspace">userspace पे जाए </string>
+ <string name="type_name_kernel_module">कर्नेल मॉड्यूल</string>
+ <string name="unknown_error">अज्ञात त्रुटि</string>
+ <string name="version_summary">%1$s बैकएंड %2$s</string>
+ <string name="version_summary_checking">%s बैकएंड संस्करण की जाँच कर रहा है</string>
+ <string name="version_summary_unknown">अज्ञात %s संस्करण</string>
+ <string name="version_title">WireGuard for Android v%s</string>
+ <string name="vpn_not_authorized_error">वीपीएन सेवा उपयोगकर्ता द्वारा अधिकृत नहीं है</string>
+ <string name="vpn_start_error">एंड्रॉयड वीपीएन सेवा प्रारंभ करने में असमर्थ</string>
+ <string name="zip_export_error">टनल का निर्यात करने में असमर्थ: %s</string>
+ <string name="zip_export_success">“%s” पर सहेजा गया</string>
+ <string name="zip_export_summary">ज़िप फ़ाइल को डाउनलोड फ़ोल्डर में सहेजा जाएगा</string>
+ <string name="zip_export_title">जिप फाइल के लिए टनल को एक्सपोर्ट करें</string>
+ <string name="key_length_error">चाबी की लम्बाई गलत </string>
+ <string name="key_contents_error">चाबी में खराब वर्ण</string>
+</resources>
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
new file mode 100644
index 00000000..d7dd3d1a
--- /dev/null
+++ b/ui/src/main/res/values-in/strings.xml
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="other">Tidak dapat menghapus %d terowongan: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="other">Berhasil menghapus terowongan %d</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="other">%d terowongan dipilih</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="other">Mengimpor %1$d terowongan dari %2$d</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="other">Mengimpor %d terowongan</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="other">%d Aplikasi Dikecualikan</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="other">%d Aplikasi Disertakan</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="other">%d dikecualikan</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="other">%d termasuk</item>
+ </plurals>
+ <string name="all_applications">Semua aplikasi</string>
+ <string name="exclude_from_tunnel">Kecualikan</string>
+ <string name="include_in_tunnel">Berlaku hanya untuk</string>
+ <plurals name="include_n_applications">
+ <item quantity="other">Aplikasi %d yang disertakan</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="other">Aplikasi %d yang disertakan</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="other">setiap 10 detik</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="other">detik</item>
+ </plurals>
+ <string name="use_all_applications">Gunakan semua aplikasi</string>
+ <string name="add_peer">Tambahkan rekan</string>
+ <string name="addresses">Alamat</string>
+ <string name="applications">Aplikasi</string>
+ <string name="allow_remote_control_intents_summary_off">Aplikasi eksternal tidak dapat mengalihkan tunnel (disarankan)</string>
+ <string name="allow_remote_control_intents_summary_on">Apl eksternal dapat mengalihkan tunnel (lanjutan)</string>
+ <string name="allow_remote_control_intents_title">Izinkan kendali jarak jauh</string>
+ <string name="allowed_ips">IP diizinkan</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 di %2$s</string>
+ <string name="bad_config_explanation_pka">: Harus positif dan tidak lebih dari 65535</string>
+ <string name="bad_config_explanation_positive_number">: Harus positif</string>
+ <string name="bad_config_explanation_udp_port">: Harus nomor porta UDP yang sah</string>
+ <string name="bad_config_reason_invalid_key">Kunci tidak sah</string>
+ <string name="bad_config_reason_invalid_number">Bilngan tidak sah</string>
+ <string name="bad_config_reason_invalid_value">Nilai tidak sah</string>
+ <string name="bad_config_reason_missing_attribute">Atribut hilang</string>
+ <string name="bad_config_reason_missing_section">Bagian hilang</string>
+ <string name="bad_config_reason_syntax_error">Galat sintaks</string>
+ <string name="bad_config_reason_unknown_attribute">Atribut tak diketahui</string>
+ <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>
+ <string name="config_file_exists_error">Sudah ada konfigurasi berkas “%s”</string>
+ <string name="config_not_found_error">Tidak menemukan konfigurasi berkas “%s”</string>
+ <string name="config_rename_error">Tidak bisa mengganti nama konfigurasi “%s”</string>
+ <string name="config_save_error">Konfigurasi “%1$s”: %2$s tidak bisa disimpan</string>
+ <string name="config_save_success">Konfigurasi “%s” berhasil disimpan</string>
+ <string name="create_activity_title">Buat tunel WireGuard</string>
+ <string name="create_bin_dir_error">Tidak dapat membuat direktori biner lokal</string>
+ <string name="create_downloads_file_error">Tidak dapat membuat file di direktori download</string>
+ <string name="create_empty">Buat dari awal</string>
+ <string name="create_from_file">Impor dari berkas atau arsip</string>
+ <string name="create_from_qr_code">Pindai dari kode QR</string>
+ <string name="create_output_dir_error">Tidak dapat membuat direktori keluaran</string>
+ <string name="create_temp_dir_error">Tidak dapat membuat direktori lokal sementara</string>
+ <string name="create_tunnel">Buat tunel</string>
+ <string name="copied_to_clipboard">%s Disalin ke Clipboard</string>
+ <string name="dark_theme_summary_off">Saat ini menggunakan tema terang (siang)</string>
+ <string name="dark_theme_summary_on">Saat ini menggunakan tema gelap (malam)</string>
+ <string name="dark_theme_title">Gunakan tema gelap</string>
+ <string name="delete">Hapus</string>
+ <string name="tv_delete">Pilih tunnel yang akan dihapus</string>
+ <string name="tv_select_a_storage_drive">Pilih lokasi penyimpanan</string>
+ <string name="tv_no_file_picker">Silakan instal aplikasi file manajer untuk memilih file</string>
+ <string name="tv_add_tunnel_get_started">Tambahkan tunnel untuk memulai</string>
+ <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>
+ <string name="error_fetching_apps">Kesalahan mengambil daftar aplikasi: %s</string>
+ <string name="error_root">Izinkan akses root dan coba lagi</string>
+ <string name="error_up">Kesalahan pada tunel: %s</string>
+ <string name="exclude_private_ips">Kecualikan IP pribadi</string>
+ <string name="generate_new_private_key">Buat kunci privat baru</string>
+ <string name="generic_error">Eror “%s” Tidak diketahui</string>
+ <string name="hint_automatic">(otomatis)</string>
+ <string name="hint_generated">(generate)</string>
+ <string name="hint_optional">(pilihan)</string>
+ <string name="hint_optional_discouraged">(opsional, tidak disarankan)</string>
+ <string name="hint_random">(acak)</string>
+ <string name="illegal_filename_error">Nama file “%s” ilegal</string>
+ <string name="import_error">Tunel %s tidak bisa diimpor</string>
+ <string name="import_from_qr_code">Mengimpor tunel dari kode QR</string>
+ <string name="import_success">“%s” Sudah diimpor</string>
+ <string name="interface_title">Antarmuka</string>
+ <string name="key_contents_error">Karakter buruk pada kunci</string>
+ <string name="key_length_error">Panjang kunci salah</string>
+ <string name="key_length_explanation_base64">: Kunci WireGuard base64 harus terdiri dari 44 karakter (32 bit)</string>
+ <string name="key_length_explanation_binary">: Kunci WireGuard harus terdiri dari 32 bit</string>
+ <string name="key_length_explanation_hex">: Kunci hex WireGuard Harus terdiri dari 64 karakter (32 bit)</string>
+ <string name="listen_port">Isi port</string>
+ <string name="log_export_error">Log tidak bisa diekspor: %s</string>
+ <string name="log_export_subject">Berkas Log WireGuard Android</string>
+ <string name="log_export_success">Simpan ke “%s”</string>
+ <string name="log_export_title">Ekspor file log</string>
+ <string name="log_saver_activity_label">Simpan log</string>
+ <string name="log_viewer_pref_summary">Log dapat membantu dengan pengawakutuan</string>
+ <string name="log_viewer_pref_title">Lihat log aplikasi</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Tidak bisa menjalankan logcat: </string>
+ <string name="module_enabler_disabled_summary">Modul kernel eksperimental dapat meningkatkan kinerja</string>
+ <string name="module_enabler_disabled_title">Aktifkan backend modul kernel</string>
+ <string name="module_enabler_enabled_summary">Backend userspace yang lebih lambat dapat meningkatkan stabilitas</string>
+ <string name="module_enabler_enabled_title">Nonaktifkan backend modul kernel</string>
+ <string name="module_installer_error">Ada yang salah. Silakan coba lagi</string>
+ <string name="module_installer_initial">Modul kernel eksperimental dapat meningkatkan kinerja</string>
+ <string name="module_installer_not_found">Tidak tersedia modul untuk perangkat anda</string>
+ <string name="module_installer_title">Unduh dan pasang modul kernel</string>
+ <string name="module_installer_working">Mengunduh and memasang…</string>
+ <string name="module_version_error">Tidak dapat menentukan versi modul kernel</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Menyalakan satu tunel akan mematikan semuanya</string>
+ <string name="multiple_tunnels_summary_on">Beberapa tunel dapat dihidupkan secara bersamaan</string>
+ <string name="multiple_tunnels_title">Izinkan beberapa sekaligus tunel</string>
+ <string name="name">Nama</string>
+ <string name="no_config_error">Lihat tunel tanpa konfigurasi</string>
+ <string name="no_configs_error">Tidak ditemukan konfigurasi</string>
+ <string name="no_tunnels_error">Tidak ada tunel</string>
+ <string name="parse_error_generic">rangkaian</string>
+ <string name="parse_error_inet_address">Alamat IP</string>
+ <string name="parse_error_inet_endpoint">titik akhir</string>
+ <string name="parse_error_inet_network">Jaringan IP</string>
+ <string name="parse_error_integer">angka</string>
+ <string name="parse_error_reason">%1$s “%2$s” Tidak dapat diuraikan</string>
+ <string name="peer">Rekan</string>
+ <string name="permission_description">mengontrol terowongan WireGuard, mengaktifkan dan menonaktifkannya sesuka hati, berpotensi salah melalu lintas Internet</string>
+ <string name="permission_label">Kontrol tunel WireGuard</string>
+ <string name="persistent_keepalive">Keepalive persisten</string>
+ <string name="pre_shared_key">Kunci Pra-bersama</string>
+ <string name="pre_shared_key_enabled">diaktifkan</string>
+ <string name="private_key">Kunci pribadi</string>
+ <string name="public_key">Kunci publik</string>
+ <string name="qr_code_hint">Tips: generate dengan `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="restore_on_boot_summary_off">Tunel yang diaktifkan tidak akan ditampilkan saat boot</string>
+ <string name="restore_on_boot_summary_on">Tunel yang diaktifkan akan dimunculkan saat boot</string>
+ <string name="restore_on_boot_title">Pulihkan saat boot</string>
+ <string name="save">Simpan</string>
+ <string name="select_all">Pilih semua</string>
+ <string name="settings">Pengaturan</string>
+ <string name="shell_exit_status_read_error">Shell tidak dapat membaca status keluar</string>
+ <string name="shell_marker_count_error">Shell diharapkan 4 nilai, diterima %d</string>
+ <string name="shell_start_error">Gagal memulai shell: %d</string>
+ <string name="success_application_will_restart">Berhasil. Sekarang aplikasi akan memulai ulang…</string>
+ <string name="toggle_all">Matikan semua</string>
+ <string name="toggle_error">Tunel WireGuard %s gagal dialihkan</string>
+ <string name="tools_installer_already">wg and wg-quick sudah terpasang</string>
+ <string name="tools_installer_failure">Tidak dapat memasang alat command-line (tidak root?)</string>
+ <string name="tools_installer_initial">Pasang alat opsional untuk skrip</string>
+ <string name="tools_installer_initial_magisk">Pasang alat opsional untuk skrip sebagai modul Magisk</string>
+ <string name="tools_installer_initial_system">Pasang alat opsional untuk skrip ke dalam partisi sistem</string>
+ <string name="tools_installer_success_magisk">wg and wg-quick dipasang sebagai modul Magisk (diperlukan mulai ulang)</string>
+ <string name="tools_installer_success_system">wg and wg-quick dipasang ke dalam partisi sistem</string>
+ <string name="tools_installer_title">Pasang alat command line</string>
+ <string name="tools_installer_working">Memasang wg and wg-quick</string>
+ <string name="tools_unavailable_error">Alat yang diperlukan tidak tersedia</string>
+ <string name="transfer">Transfer</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">trm: %1$s, krm: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Tidak dapat membuat perangkat tun</string>
+ <string name="tunnel_config_error">Tidak bisa mengkonfigurasikan tunel (wg-quick %d dikembalikan)</string>
+ <string name="tunnel_create_error">Tunel %s tidak bisa dibuat</string>
+ <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_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>
+ <string name="tunnel_rename_error">Nama tunel %s tidak bisa diganti</string>
+ <string name="tunnel_rename_success">Berhasil mengganti nama tunnel ke “%s”</string>
+ <string name="type_name_go_userspace">Ke userspace</string>
+ <string name="type_name_kernel_module">Modul kernel</string>
+ <string name="unknown_error">Eror tidak diketahui</string>
+ <string name="version_summary">%1$s dengan v%2$s</string>
+ <string name="version_summary_checking">Mengecek versi backend %s</string>
+ <string name="version_summary_unknown">Versi %s Tidak diketahui</string>
+ <string name="version_title">WireGuard untuk Android v%s</string>
+ <string name="vpn_not_authorized_error">Layanan VPN tidak diotorisasi oleh pengguna</string>
+ <string name="vpn_start_error">Tidak dapat memulai layanan VPN Android</string>
+ <string name="zip_export_error">Tunel %s tidak bisa diekspor</string>
+ <string name="zip_export_success">Disimpan ke “%s”</string>
+ <string name="zip_export_summary">File Zip akan disimpan di folder download</string>
+ <string name="zip_export_title">Ekspor tunel ke file zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Otentikasi untuk mengekspor terowongan</string>
+ <string name="biometric_prompt_private_key_title">Otentikasi untuk melihat kunci privat</string>
+ <string name="biometric_auth_error">Otentikasi gagal</string>
+ <string name="biometric_auth_error_reason">Otentikasi gagal: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-it/strings.xml b/ui/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..8fba2592
--- /dev/null
+++ b/ui/src/main/res/values-it/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Impossibile eliminare %d tunnel: %s</item>
+ <item quantity="other">Impossibile eliminare %d tunnel: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tunnel eliminato correttamente</item>
+ <item quantity="other">%d tunnel eliminati correttamente</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunnel selezionato</item>
+ <item quantity="other">%d tunnel selezionati</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importato %d di %d tunnel</item>
+ <item quantity="other">Importati %d di %d tunnel</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Importato %d tunnel</item>
+ <item quantity="other">Importati %d tunnel</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d applicazione esclusa</item>
+ <item quantity="other">%d applicazioni escluse</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d applicazione inclusa</item>
+ <item quantity="other">%d applicazioni incluse</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d escluse</item>
+ <item quantity="other">%d escluse</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inclusa</item>
+ <item quantity="other">%d incluse</item>
+ </plurals>
+ <string name="all_applications">Tutte le applicazioni</string>
+ <string name="exclude_from_tunnel">Escludi</string>
+ <string name="include_in_tunnel">Includi solo</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Includi %d applicazione</item>
+ <item quantity="other">Includi %d applicazioni</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Escludi %d applicazione</item>
+ <item quantity="other">Escludi %d applicazioni</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">ogni secondo</item>
+ <item quantity="other">ogni %d secondi</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">secondo</item>
+ <item quantity="other">secondi</item>
+ </plurals>
+ <string name="use_all_applications">Utilizza tutte le applicazioni</string>
+ <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 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>
+ <string name="bad_config_error">%1$s in %2$s</string>
+ <string name="bad_config_explanation_pka">: deve essere positivo e non maggiore di 65535</string>
+ <string name="bad_config_explanation_positive_number">: deve essere positivo</string>
+ <string name="bad_config_explanation_udp_port">: deve essere un numero di porta UDP valido</string>
+ <string name="bad_config_reason_invalid_key">Chiave non valida</string>
+ <string name="bad_config_reason_invalid_number">Numero non valido</string>
+ <string name="bad_config_reason_invalid_value">Valore non valido</string>
+ <string name="bad_config_reason_missing_attribute">Attributo mancante</string>
+ <string name="bad_config_reason_missing_section">Sezione mancante</string>
+ <string name="bad_config_reason_syntax_error">Errore di sintassi</string>
+ <string name="bad_config_reason_unknown_attribute">Attributo sconosciuto</string>
+ <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>
+ <string name="config_file_exists_error">Il file di configurazione “%s” esiste già</string>
+ <string name="config_not_found_error">File di configurazione “%s” non trovato</string>
+ <string name="config_rename_error">Impossibile rinominare il file di configurazione “%s”</string>
+ <string name="config_save_error">Impossibile salvare la configurazione per “%1$s”: %2$s</string>
+ <string name="config_save_success">Configurazione per “%s” salvata correttamente</string>
+ <string name="create_activity_title">Crea un tunnel WireGuard</string>
+ <string name="create_bin_dir_error">Impossibile creare cartella locale binari</string>
+ <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">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>
+ <string name="copied_to_clipboard">%s copiato negli appunti</string>
+ <string name="dark_theme_summary_off">Stai usando il tema chiaro (giorno)</string>
+ <string name="dark_theme_summary_on">Stai usando il tema scuro (notte)</string>
+ <string name="dark_theme_title">Usa tema scuro</string>
+ <string name="delete">Elimina</string>
+ <string name="tv_delete">Seleziona il tunnel da eliminare</string>
+ <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">(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>
+ <string name="import_error">Impossibile importare il tunnel: %s</string>
+ <string name="import_from_qr_code">Importa tunnel da codice QR</string>
+ <string name="import_success">Importato “%s”</string>
+ <string name="interface_title">Interfaccia</string>
+ <string name="key_contents_error">Caratteri non validi nella chiave</string>
+ <string name="key_length_error">Lunghezza non valida della chiave</string>
+ <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>
+ <string name="log_export_success">Salvato in “%s”</string>
+ <string name="log_export_title">Esporta file di log</string>
+ <string name="log_saver_activity_label">Salva log</string>
+ <string name="log_viewer_pref_summary">I log possono aiutare in fase di debug</string>
+ <string name="log_viewer_pref_title">Visualizza log dell\'applicazione</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Impossibile eseguire logcat: </string>
+ <string name="module_enabler_disabled_summary">Il modulo sperimentale del kernel può migliorare le prestazioni</string>
+ <string name="module_enabler_disabled_title">Abilita il backend del modulo del kernel</string>
+ <string name="module_enabler_enabled_summary">Il backend in userspace più lento potrebbe migliorare la stabilità</string>
+ <string name="module_enabler_enabled_title">Disabilita il backend del modulo del kernel</string>
+ <string name="module_installer_error">Qualcosa non ha funzionato. Riprova</string>
+ <string name="module_installer_initial">Il modulo sperimentale del kernel può migliorare le prestazioni</string>
+ <string name="module_installer_not_found">Nessun modulo disponibile per il tuo dispositivo</string>
+ <string name="module_installer_title">Scarica e installa il modulo del kernel</string>
+ <string name="module_installer_working">Scaricamento e installazione…</string>
+ <string name="module_version_error">Impossibile determinare la versione modulo del kernel</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">L\'attivazione di un tunnel disattiverà gli altri</string>
+ <string name="multiple_tunnels_summary_on">Più tunnel possono essere attivati contemporaneamente</string>
+ <string name="multiple_tunnels_title">Consenti più tunnel contemporanei</string>
+ <string name="name">Nome</string>
+ <string name="no_config_error">Tentativo di attivare un tunnel senza configurazione</string>
+ <string name="no_configs_error">Nessuna configurazione trovata</string>
+ <string name="no_tunnels_error">Non esistono tunnel</string>
+ <string name="parse_error_generic">stringa</string>
+ <string name="parse_error_inet_address">Indirizzo IP</string>
+ <string name="parse_error_inet_endpoint">endpoint</string>
+ <string name="parse_error_inet_network">IP rete</string>
+ <string name="parse_error_integer">numero</string>
+ <string name="parse_error_reason">Impossibile analizzare %1$s “%2$s”</string>
+ <string name="peer">Peer</string>
+ <string name="permission_description">controllare i tunnel WireGuard, abilitando e disabilitando i tunnel a piacimento, potenzialmente dirottando il traffico Internet</string>
+ <string name="permission_label">controlla tunnel WireGuard</string>
+ <string name="persistent_keepalive">Tieni sempre attivo</string>
+ <string name="pre_shared_key">Chiave condivisa (PSK)</string>
+ <string name="pre_shared_key_enabled">abilitata</string>
+ <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>
+ <string name="save">Salva</string>
+ <string name="select_all">Seleziona tutto</string>
+ <string name="settings">Impostazioni</string>
+ <string name="shell_exit_status_read_error">La shell non riesce a leggere lo stato di uscita</string>
+ <string name="shell_marker_count_error">La shell si aspettava 4 marker, ne ha ricevuti %d</string>
+ <string name="shell_start_error">Avvio della shell non riuscito: %d</string>
+ <string name="success_application_will_restart">Successo. L\'applicazione si riavvierà…</string>
+ <string name="toggle_all">Inverti tutto</string>
+ <string name="toggle_error">Errore di commutazione tunnel WireGuard: %s</string>
+ <string name="tools_installer_already">wg e wg-quick sono già installati</string>
+ <string name="tools_installer_failure">Impossibile installare strumenti di riga di comando (non root?)</string>
+ <string name="tools_installer_initial">Installa strumenti facoltativi per script</string>
+ <string name="tools_installer_initial_magisk">Installa strumenti facoltativi per script come moduli Magisk</string>
+ <string name="tools_installer_initial_system">Installa strumenti facoltativi per script nella partizione di sistema</string>
+ <string name="tools_installer_success_magisk">wg e wg-quick installati come moduli Magisk (riavvio necessario)</string>
+ <string name="tools_installer_success_system">wg e wg-quick installati nella partizione di sistema</string>
+ <string name="tools_installer_title">Installa strumenti di riga di comando</string>
+ <string name="tools_installer_working">Installazione di wg e wg-quick</string>
+ <string name="tools_unavailable_error">Strumenti necessari non disponibili</string>
+ <string name="transfer">Trasferisci</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">Impossibile creare il dispositivo tun</string>
+ <string name="tunnel_config_error">Impossibile configurare il tunnel (wg-quick ha risposto %d)</string>
+ <string name="tunnel_create_error">Impossibile creare il tunnel: %s</string>
+ <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 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>
+ <string name="tunnel_rename_error">Impossibile rinominare il tunnel: %s</string>
+ <string name="tunnel_rename_success">Tunnel rinominato correttamente in “%s”</string>
+ <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>
+ <string name="version_title">WireGuard per Android v%s</string>
+ <string name="vpn_not_authorized_error">Servizio VPN non autorizzato dall\'utente</string>
+ <string name="vpn_start_error">Impossibile avviare il servizio VPN di Android</string>
+ <string name="zip_export_error">Impossibile esportare i tunnel: %s</string>
+ <string name="zip_export_success">Salvato in “%s”</string>
+ <string name="zip_export_summary">Il file zip verrà salvato nella cartella di download</string>
+ <string name="zip_export_title">Esporta i tunnel in un file zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Autenticati per esportare le chiavi i tunnel</string>
+ <string name="biometric_prompt_private_key_title">Autenticati per visualizzare le chiavi private</string>
+ <string name="biometric_auth_error">Autenticazione non riuscita</string>
+ <string name="biometric_auth_error_reason">Autenticazione non riuscita: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml
new file mode 100644
index 00000000..26e99af3
--- /dev/null
+++ b/ui/src/main/res/values-ja/strings.xml
@@ -0,0 +1,246 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="other">%d トンネルを削除できません: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="other">%d トンネルを削除しました</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="other">%d トンネルを選択</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="other">%2$d 個中の %1$d トンネルをインポートしました</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="other">%d 個のトンネル設定をインポート済</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="other">除外アプリ %d 個</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="other">トンネリング対象アプリ %d 個</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="other">対象外アプリ %d 個</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <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="other">%d アプリが対象</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="other">%d アプリを除外</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="other">%d 秒ごと</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <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">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_not_found_error">設定ファイル \"%s\" が見つかりません</string>
+ <string name="config_rename_error">設定ファイル \"%s\" の名前を変更できません</string>
+ <string name="config_save_error">“%1$s” の設定を保存できません: %2$s</string>
+ <string name="config_save_success">\"%s\" の設定を保存しました</string>
+ <string name="create_activity_title">WireGuard トンネルの作成</string>
+ <string name="create_bin_dir_error">ローカルバイナリディレクトリを作成できません</string>
+ <string name="create_downloads_file_error">ダウンロードディレクトリにファイルを作成できません</string>
+ <string name="create_empty">空の状態から作成</string>
+ <string name="create_from_file">ファイル、アーカイブからインポート</string>
+ <string name="create_from_qr_code">QRコードをスキャン</string>
+ <string name="create_output_dir_error">出力ディレクトリを作成できません</string>
+ <string name="create_temp_dir_error">ローカルに一時ディレクトリを作成できません</string>
+ <string name="create_tunnel">トンネルを作成</string>
+ <string name="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">♥ 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>
+ <string name="generic_error">未知の “%s” エラー</string>
+ <string name="hint_automatic">(自動)</string>
+ <string name="hint_generated">(生成済み)</string>
+ <string name="hint_optional">(任意)</string>
+ <string name="hint_optional_discouraged">(任意項目ですが、設定は推奨しません)</string>
+ <string name="hint_random">(ランダム)</string>
+ <string name="illegal_filename_error">不正なファイル名 “%s”</string>
+ <string name="import_error">トンネル設定をインポートできません: %s</string>
+ <string name="import_from_qr_code">QR コードからトンネル設定をインポートします</string>
+ <string name="import_success">“%s” をインポートしました</string>
+ <string name="interface_title">インターフェース</string>
+ <string name="key_contents_error">鍵に不正な文字があります</string>
+ <string name="key_length_error">鍵の長さが不正です</string>
+ <string name="key_length_explanation_base64">: WireGuard base64 鍵は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>
+ <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">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">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">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>
+ <string name="save">保存</string>
+ <string name="select_all">すべて選択</string>
+ <string name="settings">設定</string>
+ <string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string>
+ <string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました</string>
+ <string name="shell_start_error">シェルの起動に失敗しました: %d</string>
+ <string name="success_application_will_restart">成功。アプリケーションは再起動します…</string>
+ <string name="toggle_all">すべての状態を切り替え</string>
+ <string name="toggle_error">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">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ホスト名を解決できませんでした: “%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_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 for Android v%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-ko-rKR/strings.xml b/ui/src/main/res/values-ko-rKR/strings.xml
new file mode 100644
index 00000000..0f42c7a8
--- /dev/null
+++ b/ui/src/main/res/values-ko-rKR/strings.xml
@@ -0,0 +1,223 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="other">%d개의 터널을 삭제할 수 없습니다: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="other">%d개의 터널을 성공적으로 삭제했습니다</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="other">%d개의 터널이 선택되었습니다</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="other">%2$d개의 터널 중에서 %1$d개를 가져왔습니다</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="other">%d개의 터널을 가져왔습니다</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="other">%d개의 앱이 제외됨</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="other">%d개의 앱이 포함됨</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="other">%d개 제외됨</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <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="other">%d개의 앱을 포함함</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="other">%d개의 앱을 제외함</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="other">%d초 마다</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <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">%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_not_found_error">설정파일 \"%s\"를 찾을 수 없음</string>
+ <string name="config_rename_error">설정파일 \"%s\"의 이름을 변경할 수 없음</string>
+ <string name="config_save_error">\"%1$s\"에 대한 설정을 저장할 수 없음: %2$s</string>
+ <string name="config_save_success">\"%s\"에 대한 설정을 성공적으로 저장했습니다</string>
+ <string name="create_activity_title">WireGuard 터널 만들기</string>
+ <string name="create_bin_dir_error">로컬 바이너리 디렉터리를 만들 수 없음</string>
+ <string name="create_downloads_file_error">다운로드 디렉토리에 파일을 만들 수 없음</string>
+ <string name="create_empty">직접 만들기</string>
+ <string name="create_from_file">파일 또는 압축파일에서 불러오기</string>
+ <string name="create_from_qr_code">QR코드 스캔</string>
+ <string name="create_output_dir_error">출력 디렉토리를 만들 수 없음</string>
+ <string name="create_temp_dir_error">로컬 임시 디렉토리를 만들 수 없음</string>
+ <string name="create_tunnel">터널 만들기</string>
+ <string name="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="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>
+ <string name="error_fetching_apps">앱 목록을 받는 도중 오류 발생: %s</string>
+ <string name="error_root">관리자 권한이 필요함</string>
+ <string name="error_up">터널을 시작 시 오류 발생: %s</string>
+ <string name="exclude_private_ips">사설 IP 제외</string>
+ <string name="generate_new_private_key">새로운 개인 키 만들기</string>
+ <string name="generic_error">알려지지 않은 오류: “%s”</string>
+ <string name="hint_automatic">(자동)</string>
+ <string name="hint_generated">(생성됨)</string>
+ <string name="hint_optional">(선택사항)</string>
+ <string name="hint_optional_discouraged">(선택사항, 권장되지 않음)</string>
+ <string name="hint_random">(무작위)</string>
+ <string name="illegal_filename_error">잘못된 파일 이름: \"%s\"</string>
+ <string name="import_error">터널을 불러올 수 없음: %s</string>
+ <string name="import_from_qr_code">QR코드에서 터널 불러오기</string>
+ <string name="import_success">\"%s\" 불러옴</string>
+ <string name="interface_title">인터페이스</string>
+ <string name="key_contents_error">키에서 잘못된 글자 발견</string>
+ <string name="key_length_error">잘못된 키 길이</string>
+ <string name="key_length_explanation_base64">: WireGuard의 base64 키는 반드시 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_subject">WireGuard 안드로이드 로그 파일</string>
+ <string name="log_export_success">“%s”에 저장됨</string>
+ <string name="log_export_title">로그 파일 내보내기</string>
+ <string name="log_saver_activity_label">로그를 저장하기</string>
+ <string name="log_viewer_pref_summary">로그는 디버깅에 활용됨</string>
+ <string name="log_viewer_pref_title">앱 로그 보기</string>
+ <string name="log_viewer_title">로그</string>
+ <string name="logcat_error">logcat을 실행할 수 없음: </string>
+ <string name="module_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">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">WireGuard 터널 제어</string>
+ <string name="persistent_keepalive">Persistent keepalive</string>
+ <string name="pre_shared_key">사전 공유 키</string>
+ <string name="pre_shared_key_enabled">활성화됨</string>
+ <string name="private_key">개인 키</string>
+ <string name="public_key">공개 키</string>
+ <string name="qr_code_hint">팁: `qrencode -t ansiutf8 &lt; tunnel.conf` 로 생성가능함.</string>
+ <string name="restore_on_boot_summary_off">부팅 시 활성화된 터널들을 켜지 않음</string>
+ <string name="restore_on_boot_summary_on">부팅 시 활성화된 터널들을 켬</string>
+ <string name="restore_on_boot_title">부트 후 복구</string>
+ <string name="save">저장</string>
+ <string name="select_all">모두 선택</string>
+ <string name="settings">설정</string>
+ <string name="shell_exit_status_read_error">쉘은 종료 상태를 읽을 수 없음</string>
+ <string name="shell_marker_count_error">쉘은 4 개의 마커를 받아야 하지만 %d 개만 받음</string>
+ <string name="shell_start_error">쉘이 실행 실패함: %d</string>
+ <string name="success_application_will_restart">성공. 앱이 곧 재시작됨…</string>
+ <string name="toggle_all">모두 반전</string>
+ <string name="toggle_error">WireGuard 터널 토글링 오류: %s</string>
+ <string name="tools_installer_already">wg와 wg-quick 모두 이미 설치되었음</string>
+ <string name="tools_installer_failure">커맨드라인 도구 설치 불가 (루트 권한 필요)</string>
+ <string name="tools_installer_initial">스크립팅에 필요한 선택적 도구 설치하기</string>
+ <string name="tools_installer_initial_magisk">Magisk 모듈로 설치할 때의 스크립팅에 필요한 선택적 도구 설치하기</string>
+ <string name="tools_installer_initial_system">시스템 파티션에 설치할 때의 스크립팅에 필요한 선택적 도구 설치하기</string>
+ <string name="tools_installer_success_magisk">wg와 wg-quick가 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">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_name">터널 이름</string>
+ <string name="tunnel_on_error">터널을 켤 수 없음 (wgTurnOn이 %d를 반환함)</string>
+ <string name="tunnel_rename_error">터널 이름을 바꿀 수 없음: %s</string>
+ <string name="tunnel_rename_success">터널 이름을 \"%s\"로 변경 성공</string>
+ <string name="type_name_go_userspace">Go userspace</string>
+ <string name="type_name_kernel_module">커널 모듈</string>
+ <string name="unknown_error">알 수 없는 오류</string>
+ <string name="version_summary_checking">%s 백엔드 버전 확인 중</string>
+ <string name="version_summary_unknown">알 수 없는 버전: %s</string>
+ <string name="version_title">WireGuard for 안드로이드 v%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-night/bools.xml b/ui/src/main/res/values-night/bools.xml
new file mode 100644
index 00000000..b02fcc05
--- /dev/null
+++ b/ui/src/main/res/values-night/bools.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="light_status_bar">false</bool>
+ <bool name="light_navigation_bar">false</bool>
+</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
new file mode 100644
index 00000000..50317fe0
--- /dev/null
+++ b/ui/src/main/res/values-nl-rNL/strings.xml
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <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
new file mode 100644
index 00000000..ca51975e
--- /dev/null
+++ b/ui/src/main/res/values-no-rNO/strings.xml
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Kan ikke slette %d tunnel %s</item>
+ <item quantity="other">Kan ikke slette %d tunneler %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Slettet %d tunnel</item>
+ <item quantity="other">Slettet %d tunneler</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">Importerte %1$d av %2$d tunneler</item>
+ <item quantity="other">Importerte %1$d av %2$d tunneler</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Importerte %d tunnel</item>
+ <item quantity="other">Importerte %d tunneler</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d ekskludert app</item>
+ <item quantity="other">%d ekskluderte apper</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d inkludert app</item>
+ <item quantity="other">%d inkluderte apper</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d ekskludert</item>
+ <item quantity="other">%d ekskluderte</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inkludert</item>
+ <item quantity="other">%d inkluderte</item>
+ </plurals>
+ <string name="all_applications">Alle applikasjoner</string>
+ <string name="exclude_from_tunnel">Ekskluder</string>
+ <string name="include_in_tunnel">Inkluder kun</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Inkluder %d app</item>
+ <item quantity="other">Inkluder %d apper</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Ekskluder %d app</item>
+ <item quantity="other">Ekskluder %d apper</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">hvert %d sekund</item>
+ <item quantity="other">hvert %d sekund</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">sekund</item>
+ <item quantity="other">sekunder</item>
+ </plurals>
+ <string name="use_all_applications">Bruk alle apper</string>
+ <string name="add_peer">Legg til peer</string>
+ <string name="addresses">Adresser</string>
+ <string name="applications">Applikasjoner</string>
+ <string name="allow_remote_control_intents_summary_off">Fjernstyrte apper kan ikke toggle tunneler (anbefalt)</string>
+ <string name="allow_remote_control_intents_summary_on">Fjernstyrte apper toggle tunneler (avansert)</string>
+ <string name="allow_remote_control_intents_title">Tillat fjernstyrte apper</string>
+ <string name="allowed_ips">Tillatte IP-adresser</string>
+ <string name="bad_config_context">%1$s sin %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å være positiv og ikke mer enn 65535</string>
+ <string name="bad_config_explanation_positive_number">: Må være positiv</string>
+ <string name="bad_config_explanation_udp_port">: Må være et gyldig UDP portnummer</string>
+ <string name="bad_config_reason_invalid_key">Ugyldig tegn</string>
+ <string name="bad_config_reason_invalid_number">Ugyldig tall</string>
+ <string name="bad_config_reason_invalid_value">Ugyldig verdi</string>
+ <string name="bad_config_reason_missing_attribute">Manglende attributt</string>
+ <string name="bad_config_reason_missing_section">Manglende seksjon</string>
+ <string name="bad_config_reason_syntax_error">Syntaksfeil</string>
+ <string name="bad_config_reason_unknown_attribute">Ukjent attributt</string>
+ <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>
+ <string name="config_file_exists_error">Konfigurasjonsfil «%s» finnes allerede</string>
+ <string name="config_not_found_error">Konfigurasjonsfil «%s» ble ikke funnet</string>
+ <string name="config_rename_error">Kan ikke endre navn på konfigurasjonsfilen «%s»</string>
+ <string name="config_save_error">Kan ikke lagre konfigurasjonen for “%1$s”: %2$s</string>
+ <string name="config_save_success">Vellykket lagring av konfigurasjon for «%s»</string>
+ <string name="create_activity_title">Opprett WireGuard tunnel</string>
+ <string name="create_bin_dir_error">Kan ikke opprette lokal binærmappe</string>
+ <string name="create_downloads_file_error">Kan ikke opprette fil i nedlastingsmappen</string>
+ <string name="create_empty">Lag fra begynnelsen</string>
+ <string name="create_from_file">Importer fra fil eller arkiv</string>
+ <string name="create_from_qr_code">Skann QR-kode</string>
+ <string name="create_output_dir_error">Kan ikke opprette utdata-katalog</string>
+ <string name="create_temp_dir_error">Kan ikke opprette lokal, midlertidig mappe</string>
+ <string name="create_tunnel">Opprett tunnel</string>
+ <string name="copied_to_clipboard">%s kopiert til utklippstavlen</string>
+ <string name="dark_theme_summary_off">Bruker nå lyst (dag) tema</string>
+ <string name="dark_theme_summary_on">Bruker nå mørkt (natt) tema</string>
+ <string name="dark_theme_title">Bruk mørkt tema</string>
+ <string name="delete">Slett</string>
+ <string name="tv_delete">Velg tunnel som skal slettes</string>
+ <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>
+ <string name="generic_error">Ukjent «%s» feil</string>
+ <string name="hint_automatic">(automatisk)</string>
+ <string name="hint_generated">(generert)</string>
+ <string name="hint_optional">(valgfritt)</string>
+ <string name="hint_optional_discouraged">(valgfritt, anbefales ikke)</string>
+ <string name="hint_random">(tilfeldig)</string>
+ <string name="illegal_filename_error">Ugyldig filnavn «%s»</string>
+ <string name="import_error">Kan ikke importere tunnel: %s</string>
+ <string name="import_from_qr_code">Importer tunnel fra QR-kode</string>
+ <string name="import_success">Importerte «%s»</string>
+ <string name="interface_title">Grensesnitt</string>
+ <string name="key_contents_error">Ugyldig tegn i nøkkel</string>
+ <string name="key_length_error">Ugyldig nøkkellengde</string>
+ <string name="key_length_explanation_base64">: WireGuard base64-nøkler må være 44 tegn (32 byte)</string>
+ <string name="key_length_explanation_binary">: WireGuard nøkler må være 32 byte</string>
+ <string name="key_length_explanation_hex">: WireGuard hex-nøkler må være 64 tegn (32 byte)</string>
+ <string name="listen_port">Lytt på port</string>
+ <string name="log_export_error">Kan ikke eksportere logg: %s</string>
+ <string name="log_export_subject">WireGuard Android loggfil</string>
+ <string name="log_export_success">Lagret til «%s»</string>
+ <string name="log_export_title">Eksporter loggfil</string>
+ <string name="log_saver_activity_label">Lagre logg</string>
+ <string name="log_viewer_pref_summary">Logger kan være til hjelp med feilsøking</string>
+ <string name="log_viewer_pref_title">Vis programlogg</string>
+ <string name="log_viewer_title">Logg</string>
+ <string name="logcat_error">Kan ikke kjøre logcat: </string>
+ <string name="module_enabler_disabled_summary">Den eksperimentelle kjernemodulen kan gi bedre ytelse</string>
+ <string name="module_enabler_disabled_title">Aktiver backend for kjerne-modul</string>
+ <string name="module_enabler_enabled_summary">Backend i userspace er litt tregere men kan gi bedre stabilitet</string>
+ <string name="module_enabler_enabled_title">Deaktiver backend for kjerne-modul</string>
+ <string name="module_installer_error">Noe gikk galt. Vennligst prøv igjen</string>
+ <string name="module_installer_initial">Den eksperimentelle kjernemodulen kan gi bedre ytelse</string>
+ <string name="module_installer_not_found">Ingen moduler er tilgjengelige for din enhet</string>
+ <string name="module_installer_title">Last ned og installer kjernelenmodul</string>
+ <string name="module_installer_working">Laster ned og installerer…</string>
+ <string name="module_version_error">Kunne ikke finne versjon av kjerne-modulen</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Når en tunnel slås på vil andre slås av</string>
+ <string name="multiple_tunnels_summary_on">Flere tunneler kan være aktivert samtidig</string>
+ <string name="multiple_tunnels_title">Tillat flere samtidige tunneler</string>
+ <string name="name">Navn</string>
+ <string name="no_config_error">Prøver å få opp en tunnel uten konfigurasjon</string>
+ <string name="no_configs_error">Ingen konfigurasjoner funnet</string>
+ <string name="no_tunnels_error">Ingen tunneler er opprettet</string>
+ <string name="parse_error_generic">streng</string>
+ <string name="parse_error_inet_address">IP-adresse</string>
+ <string name="parse_error_inet_endpoint">endepunkt</string>
+ <string name="parse_error_inet_network">IP nettverk</string>
+ <string name="parse_error_integer">nummer</string>
+ <string name="parse_error_reason">Kan ikke tolke %1$s “%2$s”</string>
+ <string name="peer">Partner</string>
+ <string name="permission_description">kontroller WireGuard-tunneler, aktiver og deaktiver dem. Kan potensielt ta ned internett-tilgangen</string>
+ <string name="permission_label">kontroller WireGuard-tunneler</string>
+ <string name="persistent_keepalive">Send keepalive-pakker</string>
+ <string name="pre_shared_key">Forhåndsdelt nøkkel</string>
+ <string name="pre_shared_key_enabled">aktivert</string>
+ <string name="private_key">Privat Nøkkel</string>
+ <string name="public_key">Offentlig nøkkel</string>
+ <string name="qr_code_hint">Tips: generer med `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="restore_on_boot_summary_off">Vil ikke ta opp aktive tunneler ved oppstart</string>
+ <string name="restore_on_boot_summary_on">Vil ta opp aktive tunneler ved oppstart</string>
+ <string name="restore_on_boot_title">Gjenopprett ved oppstart</string>
+ <string name="save">Lagre</string>
+ <string name="select_all">Velg alle</string>
+ <string name="settings">Innstillinger</string>
+ <string name="shell_exit_status_read_error">Skallet kan ikke lese avslutningsstatus</string>
+ <string name="shell_marker_count_error">Skallet forventet 4 markører, mottok %d</string>
+ <string name="shell_start_error">Skallet klarte ikke å starte: %d</string>
+ <string name="success_application_will_restart">Suksess. Programmet vil nå starte på nytt…</string>
+ <string name="toggle_all">Toggle alle</string>
+ <string name="toggle_error">Feil under toggling av WireGuard tunnel: %s</string>
+ <string name="tools_installer_already">wg og wg-quick er allerede installert</string>
+ <string name="tools_installer_failure">Kunne ikke installere kommandolinjeverktøy (ingen root-tilgang?)</string>
+ <string name="tools_installer_initial">Installer valgfrie verktøy for skripting</string>
+ <string name="tools_installer_initial_magisk">Installer valgfrie verktøy for skripting som Magisk modul</string>
+ <string name="tools_installer_initial_system">Installer valgfrie verktøy for skripting på systempartisjonen</string>
+ <string name="tools_installer_success_magisk">wg og wg-quick installert som en Magisk modul (omstart kreves)</string>
+ <string name="tools_installer_success_system">wg og wg-quick installert på systempartisjonen</string>
+ <string name="tools_installer_title">Installer kommandolinjeverktøy</string>
+ <string name="tools_installer_working">Installerer wg og wg-quick</string>
+ <string name="tools_unavailable_error">Påkrevde verktøy er ikke tilgjengelig</string>
+ <string name="transfer">Overfør</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">Kan ikke opprette tun enhetsfil</string>
+ <string name="tunnel_config_error">Kunne ikke konfigurere tunnel (wg-quick returnerte %d)</string>
+ <string name="tunnel_create_error">Kan ikke opprette tunnel: %s</string>
+ <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">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>
+ <string name="vpn_not_authorized_error">VPN-tjeneste er ikke autorisert av bruker</string>
+ <string name="vpn_start_error">Kan ikke starte Android VPN-tjenesten</string>
+ <string name="zip_export_error">Kan ikke eksportere tunneler: %s</string>
+ <string name="zip_export_success">Lagret til «%s»</string>
+ <string name="zip_export_summary">Zip-filen vil bli lagret i nedlastingsmappen</string>
+ <string name="zip_export_title">Eksporter tunneler til zip-fil</string>
+ <string name="biometric_prompt_zip_exporter_title">Autentiser for å eksportere tunneler</string>
+ <string name="biometric_prompt_private_key_title">Autentiser for å vise privatnøkkel</string>
+ <string name="biometric_auth_error">Autentiseringsfeil</string>
+ <string name="biometric_auth_error_reason">Autentiseringsfeil: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-pa-rIN/strings.xml b/ui/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 00000000..10de7d9e
--- /dev/null
+++ b/ui/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d ਟਨਲ ਹਟਾਉਣ ਲਈ ਅਸਮਰੱਥ: %s</item>
+ <item quantity="other">%d ਟਨਲਾਂ ਹਟਾਉਣ ਲਈ ਅਸਮਰੱਥ: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d ਟਨਲ ਕਾਮਯਾਬੀ ਨਾਲ ਹਟਾਈ</item>
+ <item quantity="other">%d ਟਨਲਾਂ ਕਾਮਯਾਬੀ ਨਾਲ ਹਟਾਈਆਂ</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d ਟਨਲ ਚੁਣੀ</item>
+ <item quantity="other">%d ਟਨਲਾਂ ਚੁਣੀਆਂ</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%2$d ਟਨਲਾਂ ਵਿੱਚੋਂ %1$d ਇੰਪੋਰਟ ਕੀਤੀ</item>
+ <item quantity="other">%2$d ਟਨਲਾਂ ਵਿੱਚੋਂ %1$d ਇੰਪੋਰਟ ਕੀਤੀਆਂ</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d ਟਨਲ ਇੰਪੋਰਟ ਕੀਤੀ</item>
+ <item quantity="other">%d ਟਨਲਾਂ ਇੰਪੋਰਟ ਕੀਤੀਆਂ</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d ਅਲਹਿਦਾ ਕੀਤੀ ਐਪਲੀਕੇਸ਼ਨ</item>
+ <item quantity="other">%d ਅਲਹਿਦਾ ਕੀਤੀਆਂ ਐਪਲੀਕੇਸ਼ਨਾਂ</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d ਐਪਲੀਕੇਸ਼ਨ ਸਮੇਤ</item>
+ <item quantity="other">%d ਐਪਲੀਕੇਸ਼ਨਾਂ ਸਮੇਤ</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d ਅਲਹਿਦਾ ਰੱਖਿਆ</item>
+ <item quantity="other">%d ਅਲਹਿਦਾ ਰੱਖੇ</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d ਸਮੇਤ</item>
+ <item quantity="other">%d ਸਮੇਤ</item>
+ </plurals>
+ <string name="all_applications">ਸਾਰੀਆਂ ਐਪਲੀਕੇਸ਼ਨਾਂ</string>
+ <string name="exclude_from_tunnel">ਅਲਹਿਦਾ</string>
+ <string name="include_in_tunnel">ਸਿਰਫ਼ ਸ਼ਾਮਲ</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">%d ਐਪ ਸ਼ਾਮਲ ਕਰੋ</item>
+ <item quantity="other">%d ਐਪਾਂ ਸ਼ਾਮਲ ਕਰੋ</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">%d ਐਪ ਅਲਹਿਦਾ ਰੱਖੋ</item>
+ <item quantity="other">%d ਐਪਾਂ ਅਲਹਿਦਾ ਰੱਖੋ</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">ਹਰ ਸਕਿੰਟ</item>
+ <item quantity="other">ਹਰ %d ਸਕਿੰਟ</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">ਸਕਿੰਟ</item>
+ <item quantity="other">ਸਕਿੰਟ</item>
+ </plurals>
+ <string name="use_all_applications">ਸਾਰੀਆਂ ਐਪਾਂ ਵਰਤੋਂ</string>
+ <string name="add_peer">ਪੀਅਰ ਜੋੜੋ</string>
+ <string name="addresses">ਸਿਰਨਾਵੇ</string>
+ <string name="applications">ਐਪਲੀਕੇਸ਼ਨਾਂ</string>
+ <string name="allow_remote_control_intents_summary_off">ਬਾਹਰੀ ਐਪਾਂ ਟਨਲਾਂ ਨੂੰ ਬਦਲ ਨਹੀਂ ਸਕਦੀਆਂ (ਸਿਫਾਰਸ਼ੀ)</string>
+ <string name="allow_remote_control_intents_summary_on">ਬਾਹਰੀ ਐਪਾਂ ਟਨਲਾਂ ਨੂੰ ਬਦਲ ਸਕਦੀਆਂ ਹਨ (ਤਕਨੀਕੀ)</string>
+ <string name="allow_remote_control_intents_title">ਰਿਮੋਟ ਕੰਟਰੋਲ ਐਪਾਂ ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ</string>
+ <string name="allowed_ips">ਮਨਜ਼ੂਰ ਕੀਤੇ IP</string>
+ <string name="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="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="disable_config_export_title">ਸੰਰਚਨਾ ਐਕਸਪੋਰਟ ਕਰਨ ਨੂੰ ਅਸਮਰੱਥ ਕਰੋ</string>
+ <string name="disable_config_export_description">ਸੰਰਚਨਾ ਐਕਸਪੋਰਟ ਕਰਨ ਉੱਤੇ ਰੋਕ ਲਾਉਣ ਨਾਲ ਪ੍ਰਾਈਵੇਟ ਕੁੰਜੀਆਂ ਲਈ ਪਹੁੰਚ ਘਟੇਗੀ</string>
+ <string name="dns_servers">DNS ਸਰਵਰ</string>
+ <string name="edit">ਸੋਧੋ</string>
+ <string name="endpoint">ਐਂਡ-ਪੁਆਇੰਟ</string>
+ <string name="error_down">ਟਨਲ ਬੰਦ ਕਰਨ ਦੌਰਾਨ ਗ਼ਲਤੀ: %s</string>
+ <string name="error_fetching_apps">ਐਪ ਸੂਚੀ ਲੈਣ ਦੌਰਾਨ ਗ਼ਲਤੀ: %s</string>
+ <string name="error_root">ਰੂਟ ਪਹੁੰਚ ਲਵੋ ਅਤੇ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>
+ <string name="error_up">ਟਨਲ ਚਾਲੂ ਕਰਨ ਦੌਰਾਨ ਗ਼ਲਤੀ: %s</string>
+ <string name="exclude_private_ips">ਪ੍ਰਾਈਵੇਟ IP ਅਲਹਿਦਾ ਰੱਖੋ</string>
+ <string name="generate_new_private_key">ਨਵੀਂ ਪ੍ਰਾਈਵੇਟ ਕੁੰਜੀ ਬਣਾਓ</string>
+ <string name="generic_error">ਅਣਪਛਾਤੀ “%s” ਗਲਤੀ</string>
+ <string name="hint_automatic">(ਆਟੋ)</string>
+ <string name="hint_generated">(ਤਿਆਰ ਕੀਤਾ)</string>
+ <string name="hint_optional">(ਚੋਣਵਾਂ)</string>
+ <string name="hint_optional_discouraged">(ਚੋਣਵਾਂ, ਪਰ ਸਿਫਾਰਸ਼ੀ ਨਹੀਂ)</string>
+ <string name="hint_random">(ਰਲਵਾਂ)</string>
+ <string name="illegal_filename_error">ਗ਼ੈਰ-ਵਾਜਬ ਫ਼ਾਇਲ ਨਾਂ \"%s\"</string>
+ <string name="import_error">ਟਨਲ ਇੰਪੋਰਟ ਕਰਨ ਲਈ ਅਸਮਰੱਥ: %s</string>
+ <string name="import_from_qr_code">QR ਕੋਡ ਤੋਂ ਟਨਲ ਇੰਪੋਰਟ ਕਰੋ</string>
+ <string name="import_success">\"%s\" ਇੰਪੋਰਟ ਕੀਤੀ</string>
+ <string name="interface_title">ਇੰਟਰਫੇਸ</string>
+ <string name="key_contents_error">ਕੁੰਜੀ ਵਿੱਚ ਗ਼ਲਤ ਅੱਖਰ</string>
+ <string name="key_length_error">ਗ਼ਲਤ ਕੁੰਜੀ ਦੀ ਲੰਬਾਈ</string>
+ <string name="key_length_explanation_base64">: WireGuard base64 ਕੁੰਜੀਆਂ ਵਿੱਚ 44 ਅੱਖਰ ਹੋਣੇ ਚਾਹੀਦੇ ਹਨ (32 ਬਾਈਟ)</string>
+ <string name="key_length_explanation_binary">: WireGuard ਕੁੰਜੀਆਂ 32 ਬਾਈਟ ਹੋਣੀਆਂ ਚਾਹੀਦੀਆਂ ਹਨ</string>
+ <string name="key_length_explanation_hex">: WireGuard ਹੈਕਸਾ ਕੁੰਜੀਆਂ ਵਿੱਚ 64 ਅੱਖਰ ਹੋਣੇ ਚਾਹੀਦੇ ਹਨ (32 ਬਾਈਟ)</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">ਕਰਨਲ ਮੋਡੀਊਲ ਡਾਊਨਲੋਡ ਕਰਕੇ ਇੰਸਟਾਲ ਕਰੋ</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">ਵਾਇਰਗਾਰਡ ਟਨਲਾਂ ਨੂੰ ਕੰਟਰੋਲ ਕਰੋ, ਮਨਮਰਜ਼ੀ ਮੁਤਾਬਕ ਟਨਲਾਂ ਨੂੰ ਸਮਰੱਥ ਤੇ ਅਸਮਰੱਥ ਕਰੋ, ਸੰਭਾਵਿਤ ਇੰਟਰਨੈੱਟ ਟਰੈਫਿਕ ਦੀ ਦਿਸ਼ਾ ਬਦਲੋ</string>
+ <string name="permission_label">ਵਾਇਰਗਰਾਡ ਟਨਲਾਂ ਕੰਟਰੋਲ ਕਰੋ</string>
+ <string name="persistent_keepalive">ਸਥਿਰ ਲਗਾਤਾਰ ਜਾਰੀ ਰੱਖੋ</string>
+ <string name="pre_shared_key">ਪਹਿਲਾਂ-ਸਾਂਝੀ ਕੀਤੀ ਕੁੰਜੀ</string>
+ <string name="pre_shared_key_enabled">ਸਮਰੱਥ ਹੈ</string>
+ <string name="private_key">ਪ੍ਰਾਈਵੇਟ ਕੁੰਜੀ</string>
+ <string name="public_key">ਪਬਲਿਕ ਕੁੰਜੀ</string>
+ <string name="qr_code_hint">ਟੋਟਕਾ: `qrencode -t ansiutf8 &lt; tunnel.conf` ਨਾਲ ਤਿਆਰ ਕਰੋ।</string>
+ <string name="restore_on_boot_summary_off">ਬੂਟ ਕਰਨ ਸਮੇਂ ਸਮਰੱਥ ਕੀਤੀਆਂ ਟਨਲਾਂ ਨੂੰ ਚਾਲੂ ਨਹੀਂ ਕੀਤਾ ਜਾਵੇਗਾ</string>
+ <string name="restore_on_boot_summary_on">ਬੂਟ ਕਰਨ ਸਮੇਂ ਸਮਰੱਥ ਕੀਤੀਆਂ ਟਨਲਾਂ ਨੂੰ ਚਾਲੂ ਕੀਤਾ ਜਾਵੇਗਾ</string>
+ <string name="restore_on_boot_title">ਬੂਟ ਕਰਨ ਉੱਤੇ ਬਹਾਲ ਕਰੋ</string>
+ <string name="save">ਸੰਭਾਲੋ</string>
+ <string name="select_all">ਸਾਰੇ ਚੁਣੋ</string>
+ <string name="settings">ਸੈਟਿੰਗਾਂ</string>
+ <string name="shell_exit_status_read_error">ਸ਼ੈਲ ਬਾਹਰ ਜਾਣ (exit) ਸਥਿਤੀ ਪੜ੍ਹ ਨਹੀਂ ਸਕਦੀ</string>
+ <string name="shell_marker_count_error">ਸ਼ੈਲ 4 ਮਾਰਕਰਾਂ ਦੀ ਉਮੀਦ ਕਰਦੀ ਸੀ, %d ਮਿਲੇ</string>
+ <string name="shell_start_error">ਸ਼ੈਲ ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਅਸਫ਼ਲ ਹੈ: %d</string>
+ <string name="success_application_will_restart">ਕਾਮਯਾਬ। ਐਪਲੀਕੇਸ਼ਨ ਹੁਣ ਮੁੜ-ਚਾਲੂ ਹੋਵੇਗੀ…</string>
+ <string name="toggle_all">ਸਭ ਪਲਟੋ</string>
+ <string name="toggle_error">ਵਾਇਰਗਾਰਡ ਟਨਲ ਬਦਲਣ ਦੌਰਾਨ ਗ਼ਲਤੀ: %s</string>
+ <string name="tools_installer_already">wg ਅਤੇ wg-quick ਪਹਿਲਾਂ ਹੀ ਇੰਸਟਾਲ ਹਨ</string>
+ <string name="tools_installer_failure">ਕਮਾਂਡ-ਲਾਈਨ ਟੂਲ ਇੰਸਟਾਲ ਕਰਨ ਲਈ ਅਸਮਰੱਥ (ਰੂਟ ਨਹੀਂ ਹੋ?)</string>
+ <string name="tools_installer_initial">ਸਕ੍ਰਿਪਟ ਲਿਖਣ ਲਈ ਚੋਣਵੇਂ ਟੂਲ ਇੰਸਟਾਲ ਕਰੋ</string>
+ <string name="tools_installer_initial_magisk">Magisk ਮੋਡੀਊਲ ਵਜੋਂ ਸਕ੍ਰਿਪਟਾਂ ਲਿਖਣ ਲਈ ਚੋਣਵੇਂ ਟੂਲ ਇੰਸਟਾਲ ਕਰੋ</string>
+ <string name="tools_installer_initial_system">ਸਿਸਟਮ ਪਾਰਟੀਸ਼ਨ ਵਿੱਚ ਸਕ੍ਰਿਪਟ ਲਿਖਣ ਲਈ ਚੋਣਵੇਂ ਟੂਲ ਇੰਸਟਾਲ ਕਰੋ</string>
+ <string name="tools_installer_success_magisk">wg ਅਤੇ wg-quick ਨੂੰ 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">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</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_name">ਟਨਲ ਦਾ ਨਾਂ</string>
+ <string name="tunnel_on_error">ਟਨਲ ਚਾਲੂ ਕਰਨ ਲਈ ਅਸਮਰੱਥ (wgTurnOn ਨੇ %d ਵਾਪਸ ਕੀਤਾ)</string>
+ <string name="tunnel_rename_error">ਟਨਲ ਨਾਂ-ਬਦਲਣ ਲਈ ਅਸਮਰੱਥ: %s</string>
+ <string name="tunnel_rename_success">ਟਨਲ ਦਾ ਨਾਂ \"%s\" ਵਜੋਂ ਕਾਮਯਾਬੀ ਨਾਲ ਬਦਲਿਆ ਗਿਆ</string>
+ <string name="type_name_go_userspace">ਵਰਤੋਂ-ਸਪੇਸ ਤੇ ਜਾਓ</string>
+ <string name="type_name_kernel_module">ਕਰਨਲ ਮੋਡੀਊਲ</string>
+ <string name="unknown_error">ਅਣਪਛਾਤੀ ਗਲਤੀ</string>
+ <string name="version_summary_checking">%s ਬੈਕਐਂਡ ਵਰਜ਼ਨ ਦੀ ਜਾਂਚ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ</string>
+ <string name="version_summary_unknown">ਅਣਪਛਾਤਾ %s ਵਰਜਨ</string>
+ <string name="version_title">Android ਲਈ WireGuard v%s</string>
+ <string name="vpn_not_authorized_error">VPN ਸੇਵਾ ਨੂੰ ਵਰਤੋਂਕਾਰ ਨੇ ਪਰਮਾਣਿਤ ਨਹੀਂ ਕੀਤਾ</string>
+ <string name="vpn_start_error">ਐਂਡਰਾਈਡ VPN ਸੇਵਾ ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਅਸਮਰੱਥ</string>
+ <string name="zip_export_error">ਟਨਲਾਂ ਐਕਸਪੋਰਟ ਕਰਨ ਲਈ ਅਸਮਰੱਥ: %s</string>
+ <string name="zip_export_success">“%s” ਉੱਤੇ ਸੰਭਾਲਿਆ ਗਿਆ</string>
+ <string name="zip_export_summary">ਜ਼ਿਪ ਫਾਈਲ ਨੂੰ ਡਾਊਨਲੋਡ ਫੋਲਡਰ ਵਿੱਚ ਸੰਭਾਲਿਆ ਜਾਵੇਗਾ</string>
+ <string name="zip_export_title">ਟਨਲ ਨੂੰ ਜ਼ਿੱਪ ਫ਼ਾਇਲ ਵਜੋਂ ਬਰਾਮਦ ਕਰੋ</string>
+ <string name="biometric_prompt_zip_exporter_title">ਟਨਲ ਬਰਾਮਦ ਕਰਨ ਲਈ ਪਰਮਾਣਕਿਤਾ</string>
+ <string name="biometric_prompt_private_key_title">ਪ੍ਰਾਈਵੇਟ ਕੁੰਜੀ ਵੇਖਣ ਲਈ ਪਰਮਾਣਕਿਤਾ</string>
+ <string name="biometric_auth_error">ਪਰਮਾਣਿਤ ਕਰਨ ਲਈ ਅਸਫ਼ਲ</string>
+ <string name="biometric_auth_error_reason">ਪਰਮਾਣਿਤ ਕਰਨ ਲਈ ਅਸਫ਼ਲ: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-pl-rPL/strings.xml b/ui/src/main/res/values-pl-rPL/strings.xml
new file mode 100644
index 00000000..3c9de47d
--- /dev/null
+++ b/ui/src/main/res/values-pl-rPL/strings.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Nie można usunąć %d tunelu: %s</item>
+ <item quantity="few">Nie można usunąć %d tuneli: %s</item>
+ <item quantity="many">Nie można usunąć %d tuneli: %s</item>
+ <item quantity="other">Nie można usunąć %d tuneli: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Pomyślnie usunięto tunel %d</item>
+ <item quantity="few">Pomyślnie usunięto %d tunele</item>
+ <item quantity="many">Pomyślnie usunięto %d tuneli</item>
+ <item quantity="other">Pomyślnie usunięto %d tuneli</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d wybrany tunel</item>
+ <item quantity="few">%d wybrane tunele</item>
+ <item quantity="many">%d wybranych tuneli</item>
+ <item quantity="other">%d wybranych tuneli</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Zaimportowano %1$d z %2$d tuneli</item>
+ <item quantity="few">Zaimportowano %1$d z %2$d tuneli</item>
+ <item quantity="many">Zaimportowano %1$d z %2$d tuneli</item>
+ <item quantity="other">Zaimportowano %1$d z %2$d tuneli</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Zaimportowano %d tunel</item>
+ <item quantity="few">Zaimportowano %d tunele</item>
+ <item quantity="many">Zaimportowano %d tuneli</item>
+ <item quantity="other">Zaimportowano %d tuneli</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d wykluczona aplikacja</item>
+ <item quantity="few">%d wykluczone aplikacje</item>
+ <item quantity="many">%d wykluczonych aplikacji</item>
+ <item quantity="other">%d wykluczonych aplikacji</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d dołączona aplikacja</item>
+ <item quantity="few">%d dołączone aplikacje</item>
+ <item quantity="many">%d dołączonych aplikacji</item>
+ <item quantity="other">%d dołączonych aplikacji</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d wykluczona</item>
+ <item quantity="few">%d wykluczone</item>
+ <item quantity="many">%d wykluczonych</item>
+ <item quantity="other">%d wykluczonych</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d dołączona</item>
+ <item quantity="few">%d dołączone</item>
+ <item quantity="many">%d dołączonych</item>
+ <item quantity="other">%d dołączonych</item>
+ </plurals>
+ <string name="all_applications">Wszystkie aplikacje</string>
+ <string name="exclude_from_tunnel">Wyklucz</string>
+ <string name="include_in_tunnel">Dołącz tylko</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Dołącz %d aplikację</item>
+ <item quantity="few">Dołącz %d aplikacje</item>
+ <item quantity="many">Dołącz %d aplikacji</item>
+ <item quantity="other">Dołącz %d aplikacji</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Wyklucz %d aplikację</item>
+ <item quantity="few">Wyklucz %d aplikacje</item>
+ <item quantity="many">Wyklucz %d aplikacji</item>
+ <item quantity="other">Wyklucz %d aplikacji</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">co sekundę</item>
+ <item quantity="few">co %d sekundy</item>
+ <item quantity="many">co %d sekund</item>
+ <item quantity="other">co %d sekund</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">sekunda</item>
+ <item quantity="few">sekundy</item>
+ <item quantity="many">sekund</item>
+ <item quantity="other">sekundy</item>
+ </plurals>
+ <string name="use_all_applications">Użyj wszystkich</string>
+ <string name="add_peer">Dodaj klienta</string>
+ <string name="addresses">Adresy</string>
+ <string name="applications">Aplikacje</string>
+ <string name="allow_remote_control_intents_summary_off">Zewnętrzne aplikacje nie mogą przełączać tuneli (zalecane)</string>
+ <string name="allow_remote_control_intents_summary_on">Zewnętrzne aplikacje mogą przełączać tunele (zaawansowane)</string>
+ <string name="allow_remote_control_intents_title">Zezwól na kontrolowanie przez zewnętrzne aplikacje</string>
+ <string name="allowed_ips">Dozwolone adresy IP</string>
+ <string name="bad_config_context">%1$s dla %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s w %2$s</string>
+ <string name="bad_config_explanation_pka">: Musi być dodatni i nie większy niż 65535</string>
+ <string name="bad_config_explanation_positive_number">: Musi być dodatni</string>
+ <string name="bad_config_explanation_udp_port">: Musi być prawidłowym numerem portu UDP</string>
+ <string name="bad_config_reason_invalid_key">Nieprawidłowy klucz</string>
+ <string name="bad_config_reason_invalid_number">Nieprawidłowa liczba</string>
+ <string name="bad_config_reason_invalid_value">Nieprawidłowa wartość</string>
+ <string name="bad_config_reason_missing_attribute">Brakujący atrybut</string>
+ <string name="bad_config_reason_missing_section">Brakująca sekcja</string>
+ <string name="bad_config_reason_syntax_error">Błąd składni</string>
+ <string name="bad_config_reason_unknown_attribute">Nieznany atrybut</string>
+ <string name="bad_config_reason_unknown_section">Nieznana sekcja</string>
+ <string name="bad_config_reason_value_out_of_range">Wartość poza zakresem</string>
+ <string name="bad_extension_error">Plik musi 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>
+ <string name="config_file_exists_error">Plik konfiguracyjny „%s” już istnieje</string>
+ <string name="config_not_found_error">Nie znaleziono pliku konfiguracyjnego „%s”</string>
+ <string name="config_rename_error">Nie można zmienić nazwy pliku konfiguracyjnego „%s”</string>
+ <string name="config_save_error">Nie można zapisać konfiguracji dla „%1$s”: %2$s</string>
+ <string name="config_save_success">Pomyślnie zapisano konfigurację dla „%s”</string>
+ <string name="create_activity_title">Utwórz tunel WireGuard</string>
+ <string name="create_bin_dir_error">Nie można utworzyć lokalnego folderu dla plików wykonywalnych</string>
+ <string name="create_downloads_file_error">Nie można utworzyć pliku w folderze pobierania</string>
+ <string name="create_empty">Utwórz ręcznie</string>
+ <string name="create_from_file">Utwórz z pliku lub archiwum</string>
+ <string name="create_from_qr_code">Zeskanuj kod QR</string>
+ <string name="create_output_dir_error">Nie można utworzyć folderu wyjściowego</string>
+ <string name="create_temp_dir_error">Nie można utworzyć tymczasowego folderu lokalnego</string>
+ <string name="create_tunnel">Utwórz tunel</string>
+ <string name="copied_to_clipboard">%s skopiowano do schowka</string>
+ <string name="dark_theme_summary_off">Obecnie używany jest jasny motyw</string>
+ <string name="dark_theme_summary_on">Obecnie używany jest ciemny motyw</string>
+ <string name="dark_theme_title">Użyj ciemnego motywu</string>
+ <string name="delete">Usuń</string>
+ <string name="tv_delete">Wybierz tunel do usunięcia</string>
+ <string name="tv_select_a_storage_drive">Wybierz dysk pamięci</string>
+ <string name="tv_no_file_picker">Zainstaluj narzędzie do zarządzania plikami, aby przeglądać pliki</string>
+ <string name="tv_add_tunnel_get_started">Dodaj tunel, aby rozpocząć</string>
+ <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>
+ <string name="error_fetching_apps">Błąd podczas pobierania listy aplikacji: %s</string>
+ <string name="error_root">Proszę uzyskać dostęp do root-a i spróbować ponownie</string>
+ <string name="error_up">Błąd podczas otwierania tunelu: %s</string>
+ <string name="exclude_private_ips">Wyklucz prywatne adresy IP</string>
+ <string name="generate_new_private_key">Wygeneruj nowy klucz prywatny</string>
+ <string name="generic_error">Nieznany błąd „%s”</string>
+ <string name="hint_automatic">(auto.)</string>
+ <string name="hint_generated">(wygenerowany)</string>
+ <string name="hint_optional">(opcjonalnie)</string>
+ <string name="hint_optional_discouraged">(opcjonalnie, niezalecane)</string>
+ <string name="hint_random">(losowy)</string>
+ <string name="illegal_filename_error">Niedozwolona nazwa pliku „%s”</string>
+ <string name="import_error">Nie można zaimportować tunelu: %s</string>
+ <string name="import_from_qr_code">Importuj tunel za pomocą kodu QR</string>
+ <string name="import_success">Zaimportowano „%s”</string>
+ <string name="interface_title">Interfejs</string>
+ <string name="key_contents_error">Nieprawidłowe znaki w kluczu</string>
+ <string name="key_length_error">Nieprawidłowa długość klucza</string>
+ <string name="key_length_explanation_base64">: Klucze Base64 WireGuard-a muszą mieć długość 44 znaków (32 bajty)</string>
+ <string name="key_length_explanation_binary">: Klucze WireGuard-a muszą mieć wielkość 32 bajtów</string>
+ <string name="key_length_explanation_hex">: Klucze Hex WireGuard-a muszą mieć długość 64 znaków (32 bajty)</string>
+ <string name="listen_port">Port nasłuchu</string>
+ <string name="log_export_error">Nie można wyeksportować logu: %s</string>
+ <string name="log_export_subject">Plik logu programu WireGuard dla systemu Android</string>
+ <string name="log_export_success">Zapisano w „%s”</string>
+ <string name="log_export_title">Wyeksportuj plik logu</string>
+ <string name="log_saver_activity_label">Zapisz log</string>
+ <string name="log_viewer_pref_summary">Logi mogą pomóc w debugowaniu</string>
+ <string name="log_viewer_pref_title">Wyświetl log aplikacji</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Nie można uruchomić narzędzia logcat: </string>
+ <string name="module_enabler_disabled_summary">Eksperymentalny moduł jądra może poprawić wydajność</string>
+ <string name="module_enabler_disabled_title">Włącz moduł jądra</string>
+ <string name="module_enabler_enabled_summary">Wolniejsza implementacja w przestrzeni użytkownika może poprawić stabilność</string>
+ <string name="module_enabler_enabled_title">Wyłącz moduł jądra</string>
+ <string name="module_installer_error">Coś poszło nie tak. Proszę spróbować ponownie</string>
+ <string name="module_installer_initial">Eksperymentalny moduł jądra może poprawić wydajność</string>
+ <string name="module_installer_not_found">Brak dostępnych modułów dla tego urządzenia</string>
+ <string name="module_installer_title">Pobierz i zainstaluj moduł jądra</string>
+ <string name="module_installer_working">Pobieranie i instalowanie…</string>
+ <string name="module_version_error">Nie można określić wersji modułu jądra</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Włączenie jednego tunelu spowoduje wyłączenie innych</string>
+ <string name="multiple_tunnels_summary_on">Wiele tuneli może być włączonych jednocześnie</string>
+ <string name="multiple_tunnels_title">Zezwól na wiele równoległych tuneli</string>
+ <string name="name">Nazwa</string>
+ <string name="no_config_error">Próba otwarcia tunelu bez konfiguracji</string>
+ <string name="no_configs_error">Nie odnaleziono żadnych konfiguracji</string>
+ <string name="no_tunnels_error">Brak tuneli</string>
+ <string name="parse_error_generic">ciąg</string>
+ <string name="parse_error_inet_address">Adresy IP</string>
+ <string name="parse_error_inet_endpoint">punkt końcowy</string>
+ <string name="parse_error_inet_network">Sieć IP</string>
+ <string name="parse_error_integer">liczba</string>
+ <string name="parse_error_reason">Nie można przetworzyć %1$s „%2$s”</string>
+ <string name="peer">Klient</string>
+ <string name="permission_description">kontrolowanie tuneli WireGuard, włączanie i wyłączanie tuneli, potencjalnie błędne kierowanie ruchem internetowym</string>
+ <string name="permission_label">sterowanie tunelami WireGuard</string>
+ <string name="persistent_keepalive">Utrzymanie połączenia</string>
+ <string name="pre_shared_key">Klucz wstępnie udostępniony</string>
+ <string name="pre_shared_key_enabled">włączone</string>
+ <string name="private_key">Klucz prywatny</string>
+ <string name="public_key">Klucz publiczny</string>
+ <string name="qr_code_hint">Wskazówka: wygeneruj za pomocą `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="restore_on_boot_summary_off">Włączone tunele nie zostaną przywrócone podczas uruchamiania</string>
+ <string name="restore_on_boot_summary_on">Włączone tunele zostaną przywrócone podczas uruchamiania</string>
+ <string name="restore_on_boot_title">Przywróć podczas uruchamiania</string>
+ <string name="save">Zapisz</string>
+ <string name="select_all">Wybierz wszystko</string>
+ <string name="settings">Ustawienia</string>
+ <string name="shell_exit_status_read_error">Powłoka nie może odczytać statusu wyjścia</string>
+ <string name="shell_marker_count_error">Powłoka spodziewała się 4 znaczników, otrzymano %d</string>
+ <string name="shell_start_error">Nie udało się uruchomić powłoki: %d</string>
+ <string name="success_application_will_restart">Ukończono pomyślnie. Aplikacja zostanie uruchomiona ponownie…</string>
+ <string name="toggle_all">Przełącz wszystkie</string>
+ <string name="toggle_error">Błąd podczas przełączania tunelu WireGuard: %s</string>
+ <string name="tools_installer_already">Narzędzia wg i wg-quick są już zainstalowane</string>
+ <string name="tools_installer_failure">Nie można zainstalować narzędzi wiersza poleceń (brak root-a?)</string>
+ <string name="tools_installer_initial">Zainstaluj opcjonalne narzędzia do tworzenia skryptów</string>
+ <string name="tools_installer_initial_magisk">Zainstaluj opcjonalne narzędzia do tworzenia skryptów jako moduł Magisk</string>
+ <string name="tools_installer_initial_system">Zainstaluj opcjonalne narzędzia do tworzenia skryptów na partycji systemowej</string>
+ <string name="tools_installer_success_magisk">Narzędzia wg i wg-quick zostały zainstalowane jako moduł Magisk (wymagane ponowne uruchomienie)</string>
+ <string name="tools_installer_success_system">Narzędzia wg i wg-quick zostały zainstalowane na partycji systemowej</string>
+ <string name="tools_installer_title">Zainstaluj narzędzia wiersza poleceń</string>
+ <string name="tools_installer_working">Instalowanie wg i wg-quick</string>
+ <string name="tools_unavailable_error">Wymagane narzędzia są niedostępne</string>
+ <string name="transfer">Transfer</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">odebrano: %1$s, wysłano: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Nie można utworzyć urządzenia TUN</string>
+ <string name="tunnel_config_error">Nie można skonfigurować tunelu (wg-quick zwróciło %d)</string>
+ <string name="tunnel_create_error">Nie można utworzyć tunelu: %s</string>
+ <string name="tunnel_create_success">Pomyślnie utworzono tunel „%s”</string>
+ <string name="tunnel_error_already_exists">Tunel \"%s\" już istnieje</string>
+ <string name="tunnel_error_invalid_name">Nieprawidłowa nazwa</string>
+ <string name="tunnel_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>
+ <string name="tunnel_rename_error">Nie można zmienić nazwy tunelu: %s</string>
+ <string name="tunnel_rename_success">Pomyślnie zmieniono nazwę tunelu na „%s”</string>
+ <string name="type_name_go_userspace">Przestrzeń użytkownika Go</string>
+ <string name="type_name_kernel_module">Moduł jądra</string>
+ <string name="unknown_error">Nieznany błąd</string>
+ <string name="version_summary">%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>
+ <string name="vpn_not_authorized_error">Usługa VPN nie została autoryzowana przez użytkownika</string>
+ <string name="vpn_start_error">Nie można uruchomić usługi VPN systemu Android</string>
+ <string name="zip_export_error">Nie można wyeksportować tuneli: %s</string>
+ <string name="zip_export_success">Zapisano w „%s”</string>
+ <string name="zip_export_summary">Plik ZIP zostanie zapisany w folderze pobierania</string>
+ <string name="zip_export_title">Wyeksportuj tunele do pliku ZIP</string>
+ <string name="biometric_prompt_zip_exporter_title">Uwierzytelnij, aby wyeksportować tunele</string>
+ <string name="biometric_prompt_private_key_title">Uwierzytelnij, aby zobaczyć klucz prywatny</string>
+ <string name="biometric_auth_error">Błąd uwierzytelnienia</string>
+ <string name="biometric_auth_error_reason">Błąd uwierzytelnienia: %s</string>
+</resources>
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
new file mode 100644
index 00000000..48afc41b
--- /dev/null
+++ b/ui/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Não foi possível apagar %d túnel: %s</item>
+ <item quantity="other">Não foi possível apagar %d túneis: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d túnel apagado com sucesso</item>
+ <item quantity="other">%d túneis apagados com sucesso</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d túnel selecionado</item>
+ <item quantity="other">%d túneis selecionados</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importado %1$d de %2$d túneis</item>
+ <item quantity="other">Importados %1$d de %2$d túneis</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d túnel importado</item>
+ <item quantity="other">%d túneis importados</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d Aplicação Excluída</item>
+ <item quantity="other">%d Aplicações Excluídas</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Aplicação Incluída</item>
+ <item quantity="other">%d Aplicações incluídas</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d Excluída</item>
+ <item quantity="other">%d Excluídas</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d Incluída</item>
+ <item quantity="other">%d Incluídas</item>
+ </plurals>
+ <string name="all_applications">Todas as Aplicações</string>
+ <string name="exclude_from_tunnel">Excluir</string>
+ <string name="include_in_tunnel">Apenas aplicações incluídas</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Incluir %d Aplicação</item>
+ <item quantity="other">Incluir %d Aplicações</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Excluir %d Aplicação</item>
+ <item quantity="other">Exluir %d Aplicações</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">a cada %d segundo</item>
+ <item quantity="other">a cada %d segundos</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">segundo</item>
+ <item quantity="other">segundos</item>
+ </plurals>
+ <string name="use_all_applications">Usar todas as aplicações</string>
+ <string name="add_peer">Adicionar nó</string>
+ <string name="addresses">Endereços</string>
+ <string name="applications">Aplicações</string>
+ <string name="allow_remote_control_intents_summary_off">Aplicações externas não podem controlar túneis (recomendado)</string>
+ <string name="allow_remote_control_intents_summary_on">Aplicações externas podem controlar túneis (avançado)</string>
+ <string name="allow_remote_control_intents_title">Permitir controlo remoto por outras aplicações</string>
+ <string name="allowed_ips">IPs permitidos</string>
+ <string name="bad_config_context">%2$s de %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">: Tem que ser positivo e não mais do que 65535</string>
+ <string name="bad_config_explanation_positive_number">: tem que ser positivo</string>
+ <string name="bad_config_explanation_udp_port">: Tem que 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 em falta</string>
+ <string name="bad_config_reason_missing_section">Secçã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">Secção desconhecida</string>
+ <string name="bad_config_reason_value_out_of_range">Valor fora do intervalo</string>
+ <string name="bad_extension_error">O ficheiro tem que ser do tipo .conf ou .zip</string>
+ <string name="cancel">Cancelar</string>
+ <string name="config_delete_error">Não é possível apagar arquivo de configuração %s</string>
+ <string name="config_exists_error">A configuração para \"%s\" já existe</string>
+ <string name="config_file_exists_error">O ficheiro de configuração \"%s\" já existe</string>
+ <string name="config_not_found_error">O ficheiro de configuração \"%s\" não foi encontrado</string>
+ <string name="config_rename_error">Não foi possível renomear o ficheiro de configuração “%s\"</string>
+ <string name="config_save_error">Não foi possível guardar a configuração para “%1$s”: %2$s</string>
+ <string name="config_save_success">A configuração para \"%s\" foi guardada com sucesso</string>
+ <string name="create_activity_title">Criar túnel WireGuard</string>
+ <string name="create_bin_dir_error">Não foi possível criar diretoria de binários local</string>
+ <string name="create_downloads_file_error">Não foi possível criar diretoria de downloads local</string>
+ <string name="create_empty">Criar do zero</string>
+ <string name="create_from_file">Importar de ficheiro ou arquivo</string>
+ <string name="create_from_qr_code">Digitalizar a partir de código QR</string>
+ <string name="create_output_dir_error">Não foi possível criar diretoria de saída</string>
+ <string name="create_temp_dir_error">Não foi possível criar diretoria local temporária</string>
+ <string name="create_tunnel">Criar túnel</string>
+ <string name="copied_to_clipboard">%s copiado para a área de transferências</string>
+ <string name="dark_theme_summary_off">Atualmente a usar tema claro (dia)</string>
+ <string name="dark_theme_summary_on">Atualmente a usar tema escuro (noite)</string>
+ <string name="dark_theme_title">Usar tema escuro</string>
+ <string name="delete">Eliminar</string>
+ <string name="tv_delete">Selecione o túnel para apagar</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 gestão de ficheiros para procurar arquivos</string>
+ <string name="tv_add_tunnel_get_started">Adicionar um túnel para começar</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 de DNS</string>
+ <string name="edit">Editar</string>
+ <string name="endpoint">Endpoint</string>
+ <string name="error_down">Erro ao fechar túnel: %s</string>
+ <string name="error_fetching_apps">Erro ao obter lista de aplicações: %s</string>
+ <string name="error_root">Por favor, obtenha acesso root e tente novamente</string>
+ <string name="error_up">Erro ao abrir túnel: %s</string>
+ <string name="exclude_private_ips">Excluir IPs privados</string>
+ <string name="generate_new_private_key">Gerar 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 túnel: %s</string>
+ <string name="import_from_qr_code">Importar Túnel a partir de Código QR</string>
+ <string name="import_success">“%s” Importado</string>
+ <string name="interface_title">Interface</string>
+ <string name="key_contents_error">Caracteres inválidos na chave</string>
+ <string name="key_length_error">Tamanho de chave incorreto</string>
+ <string name="key_length_explanation_base64">: Chaves base64 do WireGuard têm que ter 44 caracteres (32 bytes)</string>
+ <string name="key_length_explanation_binary">: Chaves de WireGuard têm que ter 32 bytes</string>
+ <string name="key_length_explanation_hex">: Chaves hexadecimais do WireGuard têm que ter 64 caracteres (32 bytes)</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">Guardado em “%s”</string>
+ <string name="log_export_title">Exportar ficheiro de log</string>
+ <string name="log_saver_activity_label">Guardar log</string>
+ <string name="log_viewer_pref_summary">Logs podem ajudar na depuração</string>
+ <string name="log_viewer_pref_title">Ver log da aplicação</string>
+ <string name="log_viewer_title">Log</string>
+ <string name="logcat_error">Não foi possível executar o logcat: </string>
+ <string name="module_enabler_disabled_summary">O módulo experimental de kernel 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">Desabilitar módulo backend do kernel</string>
+ <string name="module_installer_error">Ocorreu um erro. Por favor, 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">Transferir e instalar módulo de kernel</string>
+ <string name="module_installer_working">A transferir e instalar…</string>
+ <string name="module_version_error">Não foi possível determinar a versão do módulo de kernel</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Ativar um túnel irá desativar todos os outros</string>
+ <string name="multiple_tunnels_summary_on">Vários túneis podem ser ativados simultaneamente</string>
+ <string name="multiple_tunnels_title">Permitir vários túneis em simultâneo</string>
+ <string name="name">Nome</string>
+ <string name="no_config_error">A tentar abrir um túnel sem configuração</string>
+ <string name="no_configs_error">Nenhuma configuração encontrada</string>
+ <string name="no_tunnels_error">Não existem túneis</string>
+ <string name="parse_error_generic">texto</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 de IP</string>
+ <string name="parse_error_integer">número</string>
+ <string name="parse_error_reason">Não foi possível analisar %1$s “%2$s”</string>
+ <string name="peer">Nó</string>
+ <string name="permission_description">controlar túneis WireGuard, ativando e desativando túneis a vontade, potencialmente desviando o tráfego na Internet</string>
+ <string name="permission_label">controlar túneis 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="restore_on_boot_summary_off">Não abrirá túneis ativos durante o arranque</string>
+ <string name="restore_on_boot_summary_on">Abrirá túneis ativos durante o arranque</string>
+ <string name="restore_on_boot_title">Restaurar no arranque</string>
+ <string name="save">Guardar</string>
+ <string name="select_all">Selecionar tudo</string>
+ <string name="settings">Definições</string>
+ <string name="shell_exit_status_read_error">A shell não pôde ler o estado de saída</string>
+ <string name="shell_marker_count_error">A shell esperava 4 marcadores, recebeu %d</string>
+ <string name="shell_start_error">A shell falhou ao iniciar: %d</string>
+ <string name="success_application_will_restart">Sucesso. A aplicação irá reiniciar…</string>
+ <string name="toggle_all">Ativar/Desativar todos</string>
+ <string name="toggle_error">Erro ao alterar o túnel 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 as 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 módulo Magisk</string>
+ <string name="tools_installer_initial_system">Instalar ferramentas opcionais para scripting na partição de sistema</string>
+ <string name="tools_installer_success_magisk">wg e wg-quick instalados como módulo Magisk (requer reinicialização)</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">A instalar wg e wg-quick</string>
+ <string name="tools_unavailable_error">Ferramentas necessárias não disponíveis</string>
+ <string name="transfer">Transferência</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 dispositivo tun</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">O túnel “%s” já existe</string>
+ <string name="tunnel_error_invalid_name">Nome inválido</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>
+ <string name="tunnel_rename_success">Túnel renomeado com sucesso para “%s”</string>
+ <string name="type_name_go_userspace">Go userspace</string>
+ <string name="type_name_kernel_module">Módulo de kernel</string>
+ <string name="unknown_error">Erro desconhecido</string>
+ <string name="version_summary_checking">A verificar versão de 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 VPN não foi autorizado pelo utilizador</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">Guardado em “%s”</string>
+ <string name="zip_export_summary">O arquivo Zip será guardado 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-ro-rRO/strings.xml b/ui/src/main/res/values-ro-rRO/strings.xml
new file mode 100644
index 00000000..16ee62f0
--- /dev/null
+++ b/ui/src/main/res/values-ro-rRO/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Nu se poate șterge %d tunel: %s</item>
+ <item quantity="few">Nu se pot șterge %d tunele: %s</item>
+ <item quantity="other">Nu se pot șterge %d de tunele: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tunel a fost șters</item>
+ <item quantity="few">%d tunele șterse</item>
+ <item quantity="other">%d de tunele șterse</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunel selectat</item>
+ <item quantity="few">%d tuneluri selectate</item>
+ <item quantity="other">%d de tuneluri selectate</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d din %2$d tuneluri importate</item>
+ <item quantity="few">%1$d din %2$d tuneluri importate</item>
+ <item quantity="other">%1$d din %2$d de tuneluri importate</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d tunel importat</item>
+ <item quantity="few">%d tuneluri importate</item>
+ <item quantity="other">%d de tuneluri importate</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d aplicație exclusă</item>
+ <item quantity="few">%d aplicații excluse</item>
+ <item quantity="other">%d de aplicații excluse</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d aplicație inclusă</item>
+ <item quantity="few">%d aplicații incluse</item>
+ <item quantity="other">%d de aplicații incluse</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d exclusă</item>
+ <item quantity="few">%d excluse</item>
+ <item quantity="other">%d excluse</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inclusă</item>
+ <item quantity="few">%d incluse</item>
+ <item quantity="other">%d incluse</item>
+ </plurals>
+ <string name="all_applications">Toate aplicațiile</string>
+ <string name="exclude_from_tunnel">Excludere</string>
+ <string name="include_in_tunnel">Includere numai</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Include %d aplicație</item>
+ <item quantity="few">Include %d aplicații</item>
+ <item quantity="other">Include %d de aplicații</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Exclude %d aplicație</item>
+ <item quantity="few">Exclude %d aplicații</item>
+ <item quantity="other">Exclude %d de aplicații</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">în fiecare secundă</item>
+ <item quantity="few">la fiecare %d secunde</item>
+ <item quantity="other">la fiecare %d secunde</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">secundă</item>
+ <item quantity="few">secunde</item>
+ <item quantity="other">secunde</item>
+ </plurals>
+ <string name="use_all_applications">Utilizează toate aplicațiile</string>
+ <string name="add_peer">Adăugare pereche</string>
+ <string name="addresses">Adrese</string>
+ <string name="applications">Aplicații</string>
+ <string name="allow_remote_control_intents_summary_off">Aplicațiile externe nu pot comuta tunelurile (recomandat)</string>
+ <string name="allow_remote_control_intents_summary_on">Aplicațiile externe pot comuta tunelurile (avansat)</string>
+ <string name="allow_remote_control_intents_title">Permite aplicații de control la distanță</string>
+ <string name="allowed_ips">IP-uri permise</string>
+ <string name="bad_config_context">%2$s pentru %1$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s în %2$s</string>
+ <string name="bad_config_explanation_pka">: Trebuie să fie pozitiv și nu mai mare decât 65535</string>
+ <string name="bad_config_explanation_positive_number">: Trebuie să fie pozitiv</string>
+ <string name="bad_config_explanation_udp_port">: Trebuie să fie un număr de port UDP valid</string>
+ <string name="bad_config_reason_invalid_key">Cheie invalidă</string>
+ <string name="bad_config_reason_invalid_number">Număr invalid</string>
+ <string name="bad_config_reason_invalid_value">Valoare invalidă</string>
+ <string name="bad_config_reason_missing_attribute">Atribuit lipsă</string>
+ <string name="bad_config_reason_missing_section">Secțiune lipsă</string>
+ <string name="bad_config_reason_syntax_error">Eroare de sintaxă</string>
+ <string name="bad_config_reason_unknown_attribute">Atribuit necunoscut</string>
+ <string name="bad_config_reason_unknown_section">Secțiune necunoscută</string>
+ <string name="bad_config_reason_value_out_of_range">Valoare în afara intervalului</string>
+ <string name="bad_extension_error">Fișierul trebuie să fie .conf sau .zip</string>
+ <string name="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>
+ <string name="config_file_exists_error">Fișierul de configurare „%s” există deja</string>
+ <string name="config_not_found_error">Fișier de configurare „%s” negăsit</string>
+ <string name="config_rename_error">Fișierul de configurare „%s” nu poate fi redenumit</string>
+ <string name="config_save_error">Nu poate fi salvată configurația pentru „%1$s”: %2$s</string>
+ <string name="config_save_success">Configurația pentru „%s” a fost salvată</string>
+ <string name="create_activity_title">Creare tunel WireGuard</string>
+ <string name="create_bin_dir_error">Nu se poate crea directorul binar local</string>
+ <string name="create_downloads_file_error">Fișierul nu poate fi creat în directorul pentru descărcări</string>
+ <string name="create_empty">Creare de la zero</string>
+ <string name="create_from_file">Importare din fișier sau arhivă</string>
+ <string name="create_from_qr_code">Scanare din cod QR</string>
+ <string name="create_output_dir_error">Nu se poate crea directorul de ieșire</string>
+ <string name="create_temp_dir_error">Nu se poate crea directorul temporar local</string>
+ <string name="create_tunnel">Creare tunel</string>
+ <string name="copied_to_clipboard">%s a fost copiat în memoria temporară</string>
+ <string name="dark_theme_summary_off">În prezent este utilizată tema luminoasă (zi)</string>
+ <string name="dark_theme_summary_on">În prezent este utilizată tema întunecată (noapte)</string>
+ <string name="dark_theme_title">Utilizare temă întunecată</string>
+ <string name="delete">Ștergere</string>
+ <string name="tv_delete">Selectează tunelul de șters</string>
+ <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>
+ <string name="generic_error">Eroare „%s” necunoscută</string>
+ <string name="hint_automatic">(auto)</string>
+ <string name="hint_generated">(generată)</string>
+ <string name="hint_optional">(opțională)</string>
+ <string name="hint_optional_discouraged">(opțională, nerecomandată)</string>
+ <string name="hint_random">(aleatorie)</string>
+ <string name="illegal_filename_error">Nume nepermis de fișier „%s”</string>
+ <string name="import_error">Tunelul nu poate fi importat: %s</string>
+ <string name="import_from_qr_code">Importare tunel din cod QR</string>
+ <string name="import_success">„%s” importat</string>
+ <string name="interface_title">Interfață</string>
+ <string name="key_contents_error">Caractere incorecte în cheie</string>
+ <string name="key_length_error">Lungime incorectă a cheii</string>
+ <string name="key_length_explanation_base64">: Cheile base64 ale WireGuard trebuie să aibă 44 de caractere (32 de octeți)</string>
+ <string name="key_length_explanation_binary">: Cheile WireGuard trebuie să aibă 32 de octeți</string>
+ <string name="key_length_explanation_hex">: Cheile hex WireGuard trebuie să aibă 64 de caractere (32 de octeți)</string>
+ <string name="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>
+ <string name="log_export_success">Salvat în „%s”</string>
+ <string name="log_export_title">Exportare fișier de jurnal</string>
+ <string name="log_saver_activity_label">Salvare jurnal</string>
+ <string name="log_viewer_pref_summary">Jurnalele pot ajuta la depanare</string>
+ <string name="log_viewer_pref_title">Vizualizare jurnal aplicație</string>
+ <string name="log_viewer_title">Jurnal</string>
+ <string name="logcat_error">Programul logcat nu poate fi executat: </string>
+ <string name="module_enabler_disabled_summary">Modulul experimental de nucleu poate îmbunătăți performanța</string>
+ <string name="module_enabler_disabled_title">Activează biblioteca modulului de nucleu</string>
+ <string name="module_enabler_enabled_summary">Biblioteca mai lentă a spațiului utilizatorului poate îmbunătăți stabilitatea</string>
+ <string name="module_enabler_enabled_title">Dezactivează biblioteca modulului de nucleu</string>
+ <string name="module_installer_error">A apărut o eroare. Încearcă din nou</string>
+ <string name="module_installer_initial">Modulul experimental de nucleu poate îmbunătăți performanța</string>
+ <string name="module_installer_not_found">Nu sunt disponibile module pentru dispozitivul tău</string>
+ <string name="module_installer_title">Descărcare și instalare modul nucleu</string>
+ <string name="module_installer_working">Se descarcă și se instalează…</string>
+ <string name="module_version_error">Nu se poate determina versiunea modulului de nucleu</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Pornirea unui tunel va opri celelalte tuneluri</string>
+ <string name="multiple_tunnels_summary_on">Mai multe tunele pot fi pornite simultan</string>
+ <string name="multiple_tunnels_title">Permite mai multe tuneluri simultane</string>
+ <string name="name">Nume</string>
+ <string name="no_config_error">Se încearcă pornirea unui tunel fără configurație</string>
+ <string name="no_configs_error">Nu au fost găsite configurații</string>
+ <string name="no_tunnels_error">Nu există tuneluri</string>
+ <string name="parse_error_generic">șir</string>
+ <string name="parse_error_inet_address">Adresă IP</string>
+ <string name="parse_error_inet_endpoint">punct final</string>
+ <string name="parse_error_inet_network">Rețea IP</string>
+ <string name="parse_error_integer">număr</string>
+ <string name="parse_error_reason">Nu se poate analiza %1$s „%2$s”</string>
+ <string name="peer">Pereche</string>
+ <string name="permission_description">controleze tuneluri WireGuard și, astfel, să activeze și să dezactiveze tuneluri în mod automat, existând posibilitatea de a dirija greșit traficul de internet</string>
+ <string name="permission_label">controleze tuneluri WireGuard</string>
+ <string name="persistent_keepalive">Mesaj keepalive persistent</string>
+ <string name="pre_shared_key">Cheie predistribuită</string>
+ <string name="pre_shared_key_enabled">activată</string>
+ <string name="private_key">Cheie privată</string>
+ <string name="public_key">Cheie publică</string>
+ <string name="qr_code_hint">Sfat: generează cu `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="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>
+ <string name="save">Salvare</string>
+ <string name="select_all">Selectare totală</string>
+ <string name="settings">Setări</string>
+ <string name="shell_exit_status_read_error">Interfața de comunicare nu poate citi starea de ieșire</string>
+ <string name="shell_marker_count_error">Interfața de comunicare a așteptat 4 marcatori, a primit %d</string>
+ <string name="shell_start_error">Interfața de comunicare nu a putut porni: %d</string>
+ <string name="success_application_will_restart">Succes. Aplicația se va reporni acum…</string>
+ <string name="toggle_all">Comutare toate</string>
+ <string name="toggle_error">Eroare la comutarea tunelului WireGuard: %s</string>
+ <string name="tools_installer_already">wg și wg-quick sunt deja instalate</string>
+ <string name="tools_installer_failure">Nu se pot instala instrumentele de linie de comandă (lipsă root?)</string>
+ <string name="tools_installer_initial">Instalare instrumente opționale pentru scriptare</string>
+ <string name="tools_installer_initial_magisk">Instalare instrumente opționale pentru scriptare ca modul Magisk</string>
+ <string name="tools_installer_initial_system">Instalare instrumente opționale pentru scriptare în partiția de sistem</string>
+ <string name="tools_installer_success_magisk">wg și wg-quick instalate ca modul Magisk (este necesară repornirea)</string>
+ <string name="tools_installer_success_system">wg și wg-quick instalate în partiția de sistem</string>
+ <string name="tools_installer_title">Instalare instrumente de linie de comandă</string>
+ <string name="tools_installer_working">Se instalează wg și wg-quick</string>
+ <string name="tools_unavailable_error">Instrumentele necesare sunt indisponibile</string>
+ <string name="transfer">Transferare</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Nu se poate crea dispozitiv TUN</string>
+ <string name="tunnel_config_error">Tunelul nu poate fi configurat (wg-quick a returnat %d)</string>
+ <string name="tunnel_create_error">Tunelul nu poate fi creat: %s</string>
+ <string name="tunnel_create_success">Tunelul „%s” a fost creat</string>
+ <string name="tunnel_error_already_exists">Tunelul „%s” există deja</string>
+ <string name="tunnel_error_invalid_name">Nume invalid</string>
+ <string name="tunnel_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>
+ <string name="tunnel_rename_error">Tunelul nu poate fi redenumit: %s</string>
+ <string name="tunnel_rename_success">Tunelul a fost redenumit ca „%s”</string>
+ <string name="type_name_go_userspace">Spațiu de utilizator Go</string>
+ <string name="type_name_kernel_module">Modul nucleu</string>
+ <string name="unknown_error">Eroare necunoscută</string>
+ <string name="version_summary">Bibliotecă %1$s %2$s</string>
+ <string name="version_summary_checking">Se verifică versiunea bibliotecii %s</string>
+ <string name="version_summary_unknown">Versiune %s necunoscută</string>
+ <string name="version_title">WireGuard pentru Android v%s</string>
+ <string name="vpn_not_authorized_error">Serviciul VPN nu este autorizat de utilizator</string>
+ <string name="vpn_start_error">Serviciul VPN Android nu poate fi pornit</string>
+ <string name="zip_export_error">Tunelurile nu pot fi exportate: %s</string>
+ <string name="zip_export_success">Salvat în „%s”</string>
+ <string name="zip_export_summary">Fișierul zip va fi salvat în dosarul pentru descărcări</string>
+ <string name="zip_export_title">Exportare tuneluri în fișierul zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Autentifică-te pentru a exporta tunelurile</string>
+ <string name="biometric_prompt_private_key_title">Autentifică-te pentru a vizualiza cheia privată</string>
+ <string name="biometric_auth_error">Eroare la autentificare</string>
+ <string name="biometric_auth_error_reason">Eroare la autentificare: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..70f38ee1
--- /dev/null
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Не удалось удалить %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="one">Успешно удален %d туннель</item>
+ <item quantity="few">Успешно удалено %d туннеля</item>
+ <item quantity="many">Успешно удалено %d туннелей</item>
+ <item quantity="other">Успешно удалено %d туннелей</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%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="one">Импортирован %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="one">Импортирован %d туннель</item>
+ <item quantity="few">Импортировано %d туннеля</item>
+ <item quantity="many">Импортировано %d туннелей</item>
+ <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>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d включенное приложение</item>
+ <item quantity="few">%d включенных приложения</item>
+ <item quantity="many">%d включенных приложений</item>
+ <item quantity="other">%d включенных приложений</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%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="one">%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="one">Включить %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="one">Исключить %d приложение</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="one">каждую секунду</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="one">секунда</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">%1$s из %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s в %2$s</string>
+ <string name="bad_config_explanation_pka">: Значение должно быть больше нуля, но меньше 65535</string>
+ <string name="bad_config_explanation_positive_number">: Значение должно быть больше нуля</string>
+ <string name="bad_config_explanation_udp_port">: Должен быть допустимым UDP-портом</string>
+ <string name="bad_config_reason_invalid_key">Неправильный ключ</string>
+ <string name="bad_config_reason_invalid_number">Неправильный номер</string>
+ <string name="bad_config_reason_invalid_value">Недопустимое значение</string>
+ <string name="bad_config_reason_missing_attribute">Несуществующий атрибут</string>
+ <string name="bad_config_reason_missing_section">Несуществующий раздел</string>
+ <string name="bad_config_reason_syntax_error">Синтаксическая ошибка</string>
+ <string name="bad_config_reason_unknown_attribute">Неизвестный атрибут</string>
+ <string name="bad_config_reason_unknown_section">Неизвестный раздел</string>
+ <string name="bad_config_reason_value_out_of_range">Значение вне диапазона</string>
+ <string name="bad_extension_error">Файл должен иметь формат .conf или .zip</string>
+ <string name="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">Создать туннель WireGuard</string>
+ <string name="create_bin_dir_error">Не удалось создать локальный бинарный каталог</string>
+ <string name="create_downloads_file_error">Не удалось создать файл в каталоге загрузок</string>
+ <string name="create_empty">Создать с нуля</string>
+ <string name="create_from_file">Импорт из файла или архива</string>
+ <string name="create_from_qr_code">Сканировать QR-код</string>
+ <string name="create_output_dir_error">Не удалось создать выходной каталог</string>
+ <string name="create_temp_dir_error">Не удалось создать временный локальный каталог</string>
+ <string name="create_tunnel">Создать туннель</string>
+ <string name="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">♥ Пожертвовать проекту 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>
+ <string name="generic_error">Неизвестная ошибка “%s”</string>
+ <string name="hint_automatic">(авто)</string>
+ <string name="hint_generated">(сгенерирован)</string>
+ <string name="hint_optional">(опционально)</string>
+ <string name="hint_optional_discouraged">(необязательно, не рекомендуется)</string>
+ <string name="hint_random">(случайно)</string>
+ <string name="illegal_filename_error">Недопустимое имя файла “%s”</string>
+ <string name="import_error">Невозможно импортировать туннель: %s</string>
+ <string name="import_from_qr_code">Импортировать туннель из QR-кода</string>
+ <string name="import_success">Импортировано “%s”</string>
+ <string name="interface_title">Интерфейс</string>
+ <string name="key_contents_error">Плохие символы в ключе</string>
+ <string name="key_length_error">Неправильная длина ключа</string>
+ <string name="key_length_explanation_base64">: ключи WireGuard base64 должны содержать 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 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">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">управлять туннелями 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="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">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">Установить дополнительные инструменты для сценариев в качестве модуля 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 Б</string>
+ <string name="transfer_gibibytes">%.2f ГиБ</string>
+ <string name="transfer_kibibytes">%.2f КиБ</string>
+ <string name="transfer_mibibytes">%.2f МиБ</string>
+ <string name="transfer_rx_tx">Принято: %1$s, Передано: %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 имя: “%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_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>
+ <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-si-rLK/strings.xml b/ui/src/main/res/values-si-rLK/strings.xml
new file mode 100644
index 00000000..f7941a1e
--- /dev/null
+++ b/ui/src/main/res/values-si-rLK/strings.xml
@@ -0,0 +1,204 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">බැහැර කළ යෙදුම් %d යි</item>
+ <item quantity="other">බැහැර කළ යෙදුම් %d යි</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">ඇතුළත් කළ යෙදුම් %d යි</item>
+ <item quantity="other">ඇතුළත් කළ යෙදුම් %d යි</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%dක් බැහැරයි</item>
+ <item quantity="other">%dක් බැහැරයි</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%dක් ඇතුළත්</item>
+ <item quantity="other">%dක් ඇතුළත්</item>
+ </plurals>
+ <string name="all_applications">සියළුම යෙදුම්</string>
+ <string name="exclude_from_tunnel">බැහැර</string>
+ <string name="include_in_tunnel">ඇතුළත් දෑ පමණි</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">යෙදුම් %dක් ඇතුළත්</item>
+ <item quantity="other">යෙදුම් %dක් ඇතුළත්</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">යෙදුම් %dක් බැහැර</item>
+ <item quantity="other">යෙදුම් %dක් බැහැර</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">සෑම තත්පරයකට</item>
+ <item quantity="other">සෑම තත්පර %d කට</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">තත්පරය</item>
+ <item quantity="other">තත්පර</item>
+ </plurals>
+ <string name="use_all_applications">සියලුම යෙදුම් භාවිතාකරන්න</string>
+ <string name="addresses">ලිපින</string>
+ <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_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_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="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="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_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="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
new file mode 100644
index 00000000..40df1d7e
--- /dev/null
+++ b/ui/src/main/res/values-sk-rSK/strings.xml
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="all_applications">Všetky aplikácie</string>
+ <string name="exclude_from_tunnel">Vylúč</string>
+ <string name="include_in_tunnel">Zahrň len</string>
+ <string name="use_all_applications">Použi všetky aplikácie</string>
+ <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>
+ <string name="bad_config_reason_invalid_key">Neplatný kľúč</string>
+ <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ú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>
+ <string name="bad_extension_error">Súbor musí byť .conf alebo .zip</string>
+ <string name="cancel">Zrušiť</string>
+ <string name="config_delete_error">Nemožete vymazať konfiguračný súbor %s</string>
+ <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">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">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>
+ <string name="dark_theme_summary_on">Momentálne používate tmavý (nočný) vzhľad</string>
+ <string name="dark_theme_title">Používať tmavý vzhľad</string>
+ <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ú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 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">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é 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 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 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í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="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">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é 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 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>
+ <string name="settings">Nastavenia</string>
+ <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 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š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>
+ <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">sem: %1$s, tam: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Nepodarilo sa vytvoriť tun zariadenie</string>
+ <string name="tunnel_create_error">Nepodarilo sa vytvoriť tunel: %s</string>
+ <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_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">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>
+ <string name="vpn_not_authorized_error">VPN služba neautorizovaná používateľom</string>
+ <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 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">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
new file mode 100644
index 00000000..c5600041
--- /dev/null
+++ b/ui/src/main/res/values-sl/strings.xml
@@ -0,0 +1,262 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d tunela ni bilo mogoče izbrisati: %s</item>
+ <item quantity="two">%d tunelov ni bilo mogoče izbrisati: %s</item>
+ <item quantity="few">%d tunelov ni bilo mogoče izbrisati: %s</item>
+ <item quantity="other">%d tunelov ni bilo mogoče izbrisati: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tunel uspešno izbrisan</item>
+ <item quantity="two">%d tunela uspešno izbrisana</item>
+ <item quantity="few">%d tuneli uspešno izbrisani</item>
+ <item quantity="other">%d tunelov uspešno izbrisanih</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunel izbran</item>
+ <item quantity="two">%d tunela izbrana</item>
+ <item quantity="few">%d tuneli izbrani</item>
+ <item quantity="other">%d tunelov izbranih</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d tunel od %2$d uvožen</item>
+ <item quantity="two">%1$d tunela od %2$d uvožena</item>
+ <item quantity="few">%1$d tuneli od %2$d uvoženi</item>
+ <item quantity="other">%1$d tunelov od %2$d uvoženih</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d tunel uvožen</item>
+ <item quantity="two">%d tunela uvožena</item>
+ <item quantity="few">%d tuneli uvoženi</item>
+ <item quantity="other">%d tunelov uvoženih</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d izvzeta aplikacija</item>
+ <item quantity="two">%d izvzeti aplikaciji</item>
+ <item quantity="few">%d izvzete aplikacije</item>
+ <item quantity="other">%d izvzetih aplikacij</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d vključena aplikacija</item>
+ <item quantity="two">%d vključeni aplikaciji</item>
+ <item quantity="few">%d vključene aplikacije</item>
+ <item quantity="other">%d vključenih aplikacij</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d izvzeta</item>
+ <item quantity="two">%d izvzeti</item>
+ <item quantity="few">%d izvzete</item>
+ <item quantity="other">%d izvzetih</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d vključena</item>
+ <item quantity="two">%d vključeni</item>
+ <item quantity="few">%d vključene</item>
+ <item quantity="other">%d vključenih</item>
+ </plurals>
+ <string name="all_applications">Vse aplikacije</string>
+ <string name="exclude_from_tunnel">Izvzemi</string>
+ <string name="include_in_tunnel">Vključi samo za</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Vključi za %d aplikacijo</item>
+ <item quantity="two">Vključi za %d aplikaciji</item>
+ <item quantity="few">Vključi za %d aplikacije</item>
+ <item quantity="other">Vključi za %d aplikacij</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Izvzemi %d aplikacijo</item>
+ <item quantity="two">Izvzemi %d aplikaciji</item>
+ <item quantity="few">Izvzemi %d aplikacije</item>
+ <item quantity="other">Izvzemi %d aplikacij</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">vsako %d sekundo</item>
+ <item quantity="two">vsaki %d sekundi</item>
+ <item quantity="few">vsake %d sekunde</item>
+ <item quantity="other">vsakih %d sekund</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">sekunda</item>
+ <item quantity="two">sekundi</item>
+ <item quantity="few">sekunde</item>
+ <item quantity="other">sekund</item>
+ </plurals>
+ <string name="use_all_applications">Vse aplikacije</string>
+ <string name="add_peer">Dodaj vrstnike</string>
+ <string name="addresses">Naslovi</string>
+ <string name="applications">Aplikacije</string>
+ <string name="allow_remote_control_intents_summary_off">Zunanjim aplikacijam ni dovoljeno preklapljati tunelov (priporočeno)</string>
+ <string name="allow_remote_control_intents_summary_on">Zunanje aplikacije lahko preklapljajo tunele (napredno)</string>
+ <string name="allow_remote_control_intents_title">Dovoljeno upravljanje preko zunanjih aplikacij</string>
+ <string name="allowed_ips">Dovoljeni naslovi IP</string>
+ <string name="bad_config_context">%1$s-ov %2$s</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">: Mora biti pozitivno in ne večje od 65535</string>
+ <string name="bad_config_explanation_positive_number">: Mora biti pozitivno</string>
+ <string name="bad_config_explanation_udp_port">: Mora biti veljavna številka vrat UDP</string>
+ <string name="bad_config_reason_invalid_key">Neveljaven ključ</string>
+ <string name="bad_config_reason_invalid_number">Neveljavno število</string>
+ <string name="bad_config_reason_invalid_value">Neveljavna vrednost</string>
+ <string name="bad_config_reason_missing_attribute">Manjka atribut</string>
+ <string name="bad_config_reason_missing_section">Manjka odsek</string>
+ <string name="bad_config_reason_syntax_error">Sintaktična napaka</string>
+ <string name="bad_config_reason_unknown_attribute">Neznan atribut</string>
+ <string name="bad_config_reason_unknown_section">Neznani odsek</string>
+ <string name="bad_config_reason_value_out_of_range">Vrednost je zunaj veljavnega območja</string>
+ <string name="bad_extension_error">Datoteka mora biti .conf ali .zip</string>
+ <string name="cancel">Prekliči</string>
+ <string name="config_delete_error">Konfiguracijske datoteke %s ni bilo mogoče izbrisati</string>
+ <string name="config_exists_error">Konfiguracija za „%s“ že obstaja</string>
+ <string name="config_file_exists_error">Konfiguracijska datoteka za „%s“ že obstaja</string>
+ <string name="config_not_found_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče najti</string>
+ <string name="config_rename_error">Konfiguracijske datoteke „%s“ ni bilo mogoče preimenovati</string>
+ <string name="config_save_error">Konfiguracijske datoteke za „%1$s“ ni bilo mogoče shraniti: %2$s</string>
+ <string name="config_save_success">Konfiguracijska datoteka za „%s“ uspešno shranjena</string>
+ <string name="create_activity_title">Ustvarite tunel WireGuard</string>
+ <string name="create_bin_dir_error">Lokalnega imenika za aplikacijo ni bilo mogoče ustvariti</string>
+ <string name="create_downloads_file_error">Datoteke v mapi prenosov ni bilo mogoče ustvariti</string>
+ <string name="create_empty">Ustvari na novo</string>
+ <string name="create_from_file">Uvozi iz datoteke ali arhiva</string>
+ <string name="create_from_qr_code">Skreniraj kodo QR</string>
+ <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č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>
+ <string name="error_fetching_apps">Napaka pri poizvedovanju seznama aplikacij: %s</string>
+ <string name="error_root">Prosim omogočite dostop root in poskusite ponovno</string>
+ <string name="error_up">Napaka pri vzpostavitvi tunela: %s</string>
+ <string name="exclude_private_ips">Izvzemi zasebne naslove IP</string>
+ <string name="generate_new_private_key">Izdelaj nov zasebni ključ</string>
+ <string name="generic_error">Neznana napaka „%s“</string>
+ <string name="hint_automatic">(samodejno)</string>
+ <string name="hint_generated">(izdelano)</string>
+ <string name="hint_optional">(izbirno)</string>
+ <string name="hint_optional_discouraged">(izbirno, ni priporočljivo)</string>
+ <string name="hint_random">(naključno)</string>
+ <string name="illegal_filename_error">Neveljavno ime datoteke „%s“</string>
+ <string name="import_error">Tunela ni bilo mogoče uvoziti: %s</string>
+ <string name="import_from_qr_code">Uvozi tunel iz kode QR</string>
+ <string name="import_success">„%s“ uvožen</string>
+ <string name="interface_title">Vmesnik</string>
+ <string name="key_contents_error">Napačni znaki v ključu</string>
+ <string name="key_length_error">Napačna dolžina ključa</string>
+ <string name="key_length_explanation_base64">: WireGuardovi ključi base64 morajo vsebovati 44 znakov (32 bajtov)</string>
+ <string name="key_length_explanation_binary">: WireGuardovi ključi morajo biti veliki 32 bajtov</string>
+ <string name="key_length_explanation_hex">: WireGuardovi ključi hex morajo biti veliki 64 znakov (32 bajtov)</string>
+ <string name="listen_port">Vrata poslušanja</string>
+ <string name="log_export_error">Dnevnika ni bilo mogoče izvoziti: %s</string>
+ <string name="log_export_subject">Dnevniška datoteka WireGuard za Android</string>
+ <string name="log_export_success">Shranjeno v „%s“</string>
+ <string name="log_export_title">Izvozi dnevniško datoteko</string>
+ <string name="log_saver_activity_label">Shrani dnevnik</string>
+ <string name="log_viewer_pref_summary">Dnevniki lahko pomagajo pri razhroščevanju</string>
+ <string name="log_viewer_pref_title">Prikaži dnevnik aplikacije</string>
+ <string name="log_viewer_title">Dnevnik</string>
+ <string name="logcat_error">Ukaza logcat ni bilo mogoče izvesti: </string>
+ <string name="module_enabler_disabled_summary">Eksperimentalni modul jedra lahko izboljša zmogljivost</string>
+ <string name="module_enabler_disabled_title">Omogoči zaledje za modul jedra</string>
+ <string name="module_enabler_enabled_summary">Počasnejše uporabniško zaledje lahko izboljša stabilnost</string>
+ <string name="module_enabler_enabled_title">Onemogoči zaledje za modul jedra</string>
+ <string name="module_installer_error">Nekaj je šlo narobe, prosimo poskusite znova</string>
+ <string name="module_installer_initial">Eksperimentalni modul jedra lahko izboljša zmogljivost</string>
+ <string name="module_installer_not_found">Za vašo napravo ni razpoložljivih modulov</string>
+ <string name="module_installer_title">Prenesi in namesti modul jedra</string>
+ <string name="module_installer_working">Prenašanje in nameščanje …</string>
+ <string name="module_version_error">Verzije modula jedra ni bilo mogoče določiti</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Vklop enega tunela bo izklopil druge</string>
+ <string name="multiple_tunnels_summary_on">Več tunelov je lahko aktiviranih sočasno</string>
+ <string name="multiple_tunnels_title">Dovoli več sočasnih tunelov</string>
+ <string name="name">Ime</string>
+ <string name="no_config_error">Poskus vzpostavitve tunela brez konfiguracije</string>
+ <string name="no_configs_error">Ni najdenih konfiguracij</string>
+ <string name="no_tunnels_error">Ni tunelov</string>
+ <string name="parse_error_generic">niz</string>
+ <string name="parse_error_inet_address">Naslov IP</string>
+ <string name="parse_error_inet_endpoint">končna točka</string>
+ <string name="parse_error_inet_network">Omrežje IP</string>
+ <string name="parse_error_integer">število</string>
+ <string name="parse_error_reason">Ni mogoče razčleniti %1$s „%2$s“</string>
+ <string name="peer">Vrstnik</string>
+ <string name="permission_description">upravljanje tunelov WireGuard, aktivacija in deaktivacija tunela, zaradi česar bo internetni promet mogoče napačno usmerjen</string>
+ <string name="permission_label">upravljanje tunelov WireGuard</string>
+ <string name="persistent_keepalive">Trajno ohranjanje povezave</string>
+ <string name="pre_shared_key">Ključ v skupni rabi</string>
+ <string name="pre_shared_key_enabled">omogočeno</string>
+ <string name="private_key">Zasebni ključ</string>
+ <string name="public_key">Javni ključ</string>
+ <string name="qr_code_hint">Namig: izdelajte z `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="restore_on_boot_summary_off">Pri zagonu telefona omogočeni tuneli ne bodo aktivirani</string>
+ <string name="restore_on_boot_summary_on">Pri zagonu telefona bodo omogočeni tuneli aktivirani</string>
+ <string name="restore_on_boot_title">Ponovna vzpostavitev ob zagonu</string>
+ <string name="save">Shrani</string>
+ <string name="select_all">Izberi vse</string>
+ <string name="settings">Nastavitve</string>
+ <string name="shell_exit_status_read_error">Lupina ne more prebrati izhodnega statusa</string>
+ <string name="shell_marker_count_error">Lupina je pričakovala 4 oznake, dobila pa je %d</string>
+ <string name="shell_start_error">Lupina se ni mogla zagnati: %d</string>
+ <string name="success_application_will_restart">Uspešno. Aplikacija se bo znova zagnala …</string>
+ <string name="toggle_all">Preklopi vse</string>
+ <string name="toggle_error">Napaka pri preklopu tunela WireGuard: %s</string>
+ <string name="tools_installer_already">wg in wg-quick sta že nameščena</string>
+ <string name="tools_installer_failure">Orodij ukazne vrstice ni bilo mogoče namestiti (ni dostopa root?)</string>
+ <string name="tools_installer_initial">Namestitev izbirnih orodij za skripte</string>
+ <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 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>
+ <string name="transfer">Prenos</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">prejeto: %1$s, poslano: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Naprave tun ni bilo mogoče ustvariti</string>
+ <string name="tunnel_config_error">Tunela ni bilo mogoče nastaviti (wg-quick je vrnil %d)</string>
+ <string name="tunnel_create_error">Tunela ni bilo mogoče ustvariti: %s</string>
+ <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_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>
+ <string name="vpn_not_authorized_error">Uporabnik ni odobril storitve VPN</string>
+ <string name="vpn_start_error">Storitve Android VPN ni bilo mogoče zagnati</string>
+ <string name="zip_export_error">Tunelov ni bilo mogoče izvoziti: %s</string>
+ <string name="zip_export_success">Shranjeno v „%s“</string>
+ <string name="zip_export_summary">Datoteka zip je bila shranjena v mapo Prenosi</string>
+ <string name="zip_export_title">Izvozi tunele v datoteko zip</string>
+ <string name="biometric_prompt_zip_exporter_title">Za izvoz tunelov se prijavite</string>
+ <string name="biometric_prompt_private_key_title">Za ogled zasebnega ključa se prijavite</string>
+ <string name="biometric_auth_error">Napaka pri overovljanju</string>
+ <string name="biometric_auth_error_reason">Napaka pri overovljanju: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-sv-rSE/strings.xml b/ui/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 00000000..910f9e05
--- /dev/null
+++ b/ui/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Kunde inte ta bort %d tunnel: %s</item>
+ <item quantity="other">Kunde inte ta bort %d tunnlar: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">Tog bort %d tunneln</item>
+ <item quantity="other">Tog bort %d tunnlar</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tunnel vald</item>
+ <item quantity="other">%d tunnlar valda</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">Importerade %1$d av %2$d tunnlar</item>
+ <item quantity="other">Importerade %1$d av %2$d tunnlar</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">Importerade %d tunnel</item>
+ <item quantity="other">Importerade %d tunnlar</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d exkluderad applikation</item>
+ <item quantity="other">%d Exkluderade applikationer</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d inkluderad applikation</item>
+ <item quantity="other">%d inkluderade applikationer</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d exkluderad</item>
+ <item quantity="other">%d exkluderade</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d inkluderad</item>
+ <item quantity="other">%d inkluderade</item>
+ </plurals>
+ <string name="all_applications">Alla applikationer</string>
+ <string name="exclude_from_tunnel">Exkludera</string>
+ <string name="include_in_tunnel">Inkludera endast</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Inkludera %d app</item>
+ <item quantity="other">Inkludera %d appar</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Exkludera %d app</item>
+ <item quantity="other">Exkludera %d appar</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">varje sekund</item>
+ <item quantity="other">var %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">Använd alla appar</string>
+ <string name="add_peer">Lägg till klient</string>
+ <string name="addresses">Adresser</string>
+ <string name="applications">Applikationer</string>
+ <string name="allow_remote_control_intents_summary_off">Externa appar kan inte växla tunnlar (rekommenderas)</string>
+ <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>
+ <string name="bad_config_explanation_positive_number">: Måste vara positivt</string>
+ <string name="bad_config_explanation_udp_port">: Måste vara ett giltigt UDP-portnummer</string>
+ <string name="bad_config_reason_invalid_key">Ogiltig nyckel</string>
+ <string name="bad_config_reason_invalid_number">Ogiltigt nummer</string>
+ <string name="bad_config_reason_invalid_value">Ogiltigt värde</string>
+ <string name="bad_config_reason_missing_attribute">Attribut saknas</string>
+ <string name="bad_config_reason_missing_section">Avsnitt saknas</string>
+ <string name="bad_config_reason_syntax_error">Syntaxfel</string>
+ <string name="bad_config_reason_unknown_attribute">Okänt attribut</string>
+ <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>
+ <string name="config_file_exists_error">Konfigurationsfil ”%s” finns redan</string>
+ <string name="config_not_found_error">Konfigurationsfil ”%s” hittades inte</string>
+ <string name="config_rename_error">Kan inte byta namn på konfigurationsfil ”%s”</string>
+ <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>
+ <string name="create_temp_dir_error">Kan inte skapa lokal temporär katalog</string>
+ <string name="create_tunnel">Skapa tunnel</string>
+ <string name="copied_to_clipboard">%s kopierades till urklipp</string>
+ <string name="dark_theme_summary_off">Använder just nu ljust (dag) tema</string>
+ <string name="dark_theme_summary_on">Använder just nu mörkt (natt) tema</string>
+ <string name="dark_theme_title">Använd mörkt tema</string>
+ <string name="delete">Radera</string>
+ <string name="tv_delete">Välj tunnel att ta bort</string>
+ <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">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">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>
+ <string name="module_enabler_disabled_summary">Den experimentella kärnmodulen kan förbättra prestanda</string>
+ <string name="module_enabler_disabled_title">Aktivera backend för kärnmodul</string>
+ <string name="module_enabler_enabled_summary">Den långsammare backend för användarrymden kan förbättra stabiliteten</string>
+ <string name="module_enabler_enabled_title">Inaktivera backend för kärnmodul</string>
+ <string name="module_installer_error">Något gick fel. Vänligen försök igen</string>
+ <string name="module_installer_initial">Den experimentella kärnmodulen kan förbättra prestanda</string>
+ <string name="module_installer_not_found">Inga moduler finns tillgängliga för din enhet</string>
+ <string name="module_installer_title">Ladda ner och installera kärnmodul</string>
+ <string name="module_installer_working">Hämtar och installerar…</string>
+ <string name="module_version_error">Kunde inte bestämma version av kärnmodulen</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Att slå på en tunnel stänger av andra</string>
+ <string name="multiple_tunnels_summary_on">Flera tunnlar kan slås på samtidigt</string>
+ <string name="multiple_tunnels_title">Tillåt flera samtidiga tunnlar</string>
+ <string name="name">Namn</string>
+ <string name="no_config_error">Försöker bygga en tunnel utan konfiguration</string>
+ <string name="no_configs_error">Inga konfigurationer hittades</string>
+ <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">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">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">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>
+ <string name="shell_start_error">Shell kunde inte starta: %d</string>
+ <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
new file mode 100644
index 00000000..57bc0ca5
--- /dev/null
+++ b/ui/src/main/res/values-tr-rTR/strings.xml
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">%d tüneli silinemiyor: %s</item>
+ <item quantity="other">%d tünelleri silinemiyor: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="one">%d tüneli başarıyla silindi</item>
+ <item quantity="other">%d tünelleri başarıyla silindi</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%d tüneli seçildi</item>
+ <item quantity="other">%d tünelleri seçildi</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="one">%1$d/%2$d tünel içe aktarıldı</item>
+ <item quantity="other">%1$d/%2$d tünel içe aktarıldı</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="one">%d tünel içe aktarıldı</item>
+ <item quantity="other">%d tünel içe aktarıldı</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="one">%d Hariç Tutulan Uygulama</item>
+ <item quantity="other">%d Hariç Tutulan Uygulamalar</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Dahil Edilen Uygulama</item>
+ <item quantity="other">%d Dahil Edilen Uygulamalar</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d hariç tutuldu</item>
+ <item quantity="other">%d hariç tutuldu</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d dahil edildi</item>
+ <item quantity="other">%d dahil edildi</item>
+ </plurals>
+ <string name="all_applications">Tüm Uygulamalar</string>
+ <string name="exclude_from_tunnel">Hariç</string>
+ <string name="include_in_tunnel">Yalnızca dahil</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">%d uygulamasını dahil et</item>
+ <item quantity="other">%d uygulamalarını dahil et</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">%d uygulamasını hariç tut</item>
+ <item quantity="other">%d uygulamalarını hariç tut</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">her saniye</item>
+ <item quantity="other">%d saniyede bir</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">saniye</item>
+ <item quantity="other">saniye</item>
+ </plurals>
+ <string name="use_all_applications">Tüm uygulamaları kullan</string>
+ <string name="add_peer">Eş ekle</string>
+ <string name="addresses">Adresler</string>
+ <string name="applications">Uygulamalar</string>
+ <string name="allow_remote_control_intents_summary_off">Harici uygulamalar tünelleri değiştiremez (önerilir)</string>
+ <string name="allow_remote_control_intents_summary_on">Harici uygulamalar tünelleri değiştirebilir (gelişmiş)</string>
+ <string name="allow_remote_control_intents_title">Uzaktan kontrol uygulamalarına izin ver</string>
+ <string name="allowed_ips">İzin verilen IP\'ler</string>
+ <string name="bad_config_context">%1$s\'in %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%2$s içinde %1$s</string>
+ <string name="bad_config_explanation_pka">: Pozitif olmalı ve 65535\'ten fazla olmamalıdır</string>
+ <string name="bad_config_explanation_positive_number">: Pozitif olmalıdır</string>
+ <string name="bad_config_explanation_udp_port">: Geçerli bir UDP Port numarası olmalıdır</string>
+ <string name="bad_config_reason_invalid_key">Geçersiz anahtar</string>
+ <string name="bad_config_reason_invalid_number">Geçersiz numara</string>
+ <string name="bad_config_reason_invalid_value">Geçersiz değer</string>
+ <string name="bad_config_reason_missing_attribute">Eksik öznitelik</string>
+ <string name="bad_config_reason_missing_section">Eksik bölüm</string>
+ <string name="bad_config_reason_syntax_error">Sözdizimi hatası</string>
+ <string name="bad_config_reason_unknown_attribute">Bilinmeyen öznitelik</string>
+ <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>
+ <string name="config_file_exists_error">“%s“ için konfigürasyon dosyası zaten var</string>
+ <string name="config_not_found_error">“%s“ konfigürasyon dosyası bulunamadı</string>
+ <string name="config_rename_error">“%s” konfigürasyon dosyası yeniden adlandırılamıyor</string>
+ <string name="config_save_error">“%1$s” için konfigürasyon kaydedilemiyor: %2$s</string>
+ <string name="config_save_success">“%s“ için konfigürasyon başarıyla kaydedildi</string>
+ <string name="create_activity_title">WireGuard Tüneli Yarat</string>
+ <string name="create_bin_dir_error">Yerel ikili dizin oluşturulamıyor</string>
+ <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 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>
+ <string name="copied_to_clipboard">%s panoya kopyalandı</string>
+ <string name="dark_theme_summary_off">Şu anda açık (gündüz) teması kullanılıyor</string>
+ <string name="dark_theme_summary_on">Şu anda karanlık (gece) teması kullanılıyor</string>
+ <string name="dark_theme_title">Koyu tema kullan</string>
+ <string name="delete">Sil</string>
+ <string name="tv_delete">Silinecek tüneli seçin</string>
+ <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>
+ <string name="generic_error">Bilinmeyen “%s” hatası</string>
+ <string name="hint_automatic">(otomatik)</string>
+ <string name="hint_generated">(oluşturuldu)</string>
+ <string name="hint_optional">(isteğe bağlı)</string>
+ <string name="hint_optional_discouraged">(isteğe bağlı, önerilmez)</string>
+ <string name="hint_random">(rastgele)</string>
+ <string name="illegal_filename_error">Geçersiz dosya ismi “%s”</string>
+ <string name="import_error">Tünel içeri aktarılamıyor: %s</string>
+ <string name="import_from_qr_code">Tüneli QR Kod ile İçe Aktar</string>
+ <string name="import_success">İçe aktarıldı “%s”</string>
+ <string name="interface_title">Arayüz</string>
+ <string name="key_contents_error">Anahtardaki yanlış karakterler</string>
+ <string name="key_length_error">Yanlış anahtar uzunluğu</string>
+ <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>
+ <string name="log_export_success">“%s” ’e kaydedildi</string>
+ <string name="log_export_title">Günlük dosyasını dışa aktar</string>
+ <string name="log_saver_activity_label">Günlüğü kaydet</string>
+ <string name="log_viewer_pref_summary">Günlükler hata ayıklamaya yardımcı olabilir</string>
+ <string name="log_viewer_pref_title">Uygulama günlüğünü görüntüle</string>
+ <string name="log_viewer_title">Günlük</string>
+ <string name="logcat_error">Logcat çalıştırılamıyor: </string>
+ <string name="module_enabler_disabled_summary">Deneysel çekirdek modülü performansı artırabilir</string>
+ <string name="module_enabler_disabled_title">Çekirdek modülü arka ucunu etkinleştir</string>
+ <string name="module_enabler_enabled_summary">Daha yavaş kullanıcı alanı arka ucu kararlılığı artırabilir</string>
+ <string name="module_enabler_enabled_title">Çekirdek modülü arka ucunu devre dışı bırak</string>
+ <string name="module_installer_error">Bir şeyler yanlış gitti. Lütfen tekrar deneyin</string>
+ <string name="module_installer_initial">Deneysel çekirdek modülü performansı artırabilir</string>
+ <string name="module_installer_not_found">Cihazınız için uygun modül yok</string>
+ <string name="module_installer_title">Çekirdek modülünü indir ve yükle</string>
+ <string name="module_installer_working">İndiriliyor ve yükleniyor…</string>
+ <string name="module_version_error">Çekirdek modülü sürümü belirlenemiyor</string>
+ <string name="mtu">MTU</string>
+ <string name="multiple_tunnels_summary_off">Bir tüneli açmak diğerlerini kapatır</string>
+ <string name="multiple_tunnels_summary_on">Aynı anda birden fazla tünel açılabilir</string>
+ <string name="multiple_tunnels_title">Birden çok eşzamanlı tünele izin verin</string>
+ <string name="name">İsim</string>
+ <string name="no_config_error">Konfigürasyonsuz bir tünel açmaya çalışılıyor</string>
+ <string name="no_configs_error">Konfigürasyon bulunamadı</string>
+ <string name="no_tunnels_error">Tünel yok</string>
+ <string name="parse_error_generic">dizi</string>
+ <string name="parse_error_inet_address">IP adresi</string>
+ <string name="parse_error_inet_endpoint">uç nokta</string>
+ <string name="parse_error_inet_network">IP ağı</string>
+ <string name="parse_error_integer">numara</string>
+ <string name="parse_error_reason">%1$s “%2$s” ayrıştırılamıyor</string>
+ <string name="peer">Eş</string>
+ <string name="permission_description">WireGuard tünellerini kontrol edin, tünelleri istediğiniz zaman etkinleştirin ve devre dışı bırakın, İnternet trafiğini potansiyel olarak yanlış yönlendirin</string>
+ <string name="permission_label">WireGuard tünellerini kontrol edin</string>
+ <string name="persistent_keepalive">Kalıcı canlı tutma</string>
+ <string name="pre_shared_key">Önceden paylaşılan anahtar</string>
+ <string name="pre_shared_key_enabled">etkinleştirildi</string>
+ <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>
+ <string name="save">Kaydet</string>
+ <string name="select_all">Tümünü seç</string>
+ <string name="settings">Ayarlar</string>
+ <string name="shell_exit_status_read_error">Kabuk, çıktı durumunu okuyamıyor</string>
+ <string name="shell_marker_count_error">Kabuk 4 işaret bekledi, %d aldı</string>
+ <string name="shell_start_error">Kabuk başlatılamadı: %d</string>
+ <string name="success_application_will_restart">Başarı. Uygulama şimdi yeniden başlayacak…</string>
+ <string name="toggle_all">Tümünü Değiştir</string>
+ <string name="toggle_error">WireGuard tüneli değiştirilirken hata oluştu: %s</string>
+ <string name="tools_installer_already">wg ve wg-quick zaten kurulu</string>
+ <string name="tools_installer_failure">Komut satırı araçları yüklenemiyor (root yok mu?)</string>
+ <string name="tools_installer_initial">Komut dosyası oluşturmak için isteğe bağlı araçları yükleyin</string>
+ <string name="tools_installer_initial_magisk">Magisk modülü olarak komut dosyası oluşturmak için isteğe bağlı araçları yükleyin</string>
+ <string name="tools_installer_initial_system">Sistem bölümüne komut dosyası oluşturmak için isteğe bağlı araçlar yükleyin</string>
+ <string name="tools_installer_success_magisk">wg ve wg-quick bir Magisk modülü olarak kuruldu (yeniden başlatma gerekir)</string>
+ <string name="tools_installer_success_system">wg ve wg-quick, sistem bölümüne yüklendi</string>
+ <string name="tools_installer_title">Komut satırı araçlarını yükleyin</string>
+ <string name="tools_installer_working">wg ve wg-quick kuruluyor</string>
+ <string name="tools_unavailable_error">Gerekli araçlar mevcut değil</string>
+ <string name="transfer">Transfer</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Tun cihazı oluşturulamıyor</string>
+ <string name="tunnel_config_error">Tünel yapılandırılamıyor (wg-quick döndürdü: %d)</string>
+ <string name="tunnel_create_error">Tünel oluşturulamıyor: %s</string>
+ <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">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>
+ <string name="tunnel_rename_error">Tünel yeniden adlandırılamıyor: %s</string>
+ <string name="tunnel_rename_success">Tünel başarıyla yeniden adlandırıldı “%s”</string>
+ <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>
+ <string name="version_title">Android için WireGuard v%s</string>
+ <string name="vpn_not_authorized_error">VPN hizmeti kullanıcı tarafından yetkilendirilmemiş</string>
+ <string name="vpn_start_error">Android VPN hizmeti başlatılamıyor</string>
+ <string name="zip_export_error">Tüneller dışa aktarılamıyor: %s</string>
+ <string name="zip_export_success">“%s” ’e kaydedildi</string>
+ <string name="zip_export_summary">Zip dosyası indirilenler klasörüne kaydedilecek</string>
+ <string name="zip_export_title">Tünelleri zip dosyasına aktar</string>
+ <string name="biometric_prompt_zip_exporter_title">Tünelleri dışa aktarmak için kimlik doğrulama</string>
+ <string name="biometric_prompt_private_key_title">Özel anahtarı görüntülemek için kimlik doğrulaması</string>
+ <string name="biometric_auth_error">Kimlik doğrulama hatası</string>
+ <string name="biometric_auth_error_reason">Kimlik doğrulama hatası: %s</string>
+</resources>
diff --git a/ui/src/main/res/values-uk-rUA/strings.xml b/ui/src/main/res/values-uk-rUA/strings.xml
new file mode 100644
index 00000000..15b51b90
--- /dev/null
+++ b/ui/src/main/res/values-uk-rUA/strings.xml
@@ -0,0 +1,270 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="one">Неможливо видалити %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="one">Успішно видалено %d тунель</item>
+ <item quantity="few">Успішно видалено %d тунелі</item>
+ <item quantity="many">Успішно видалено %d тунелів</item>
+ <item quantity="other">Успішно видалено %d тунелів</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="one">%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="one">Імпортовано %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="one">Імпортовано %d тунель</item>
+ <item quantity="few">Імпортовано %d тунелі</item>
+ <item quantity="many">Імпортовано %d тунелів</item>
+ <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>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Включений додаток</item>
+ <item quantity="few">%d Включені додатки</item>
+ <item quantity="many">%d Включених додатків</item>
+ <item quantity="other">%d Включених додатків</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%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="one">%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="one">Включити %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="one">Виключити %d додаток</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="one">кожну секунду</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="one">секунда</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">%1$s з %2$s</string>
+ <string name="bad_config_context_top_level">%s</string>
+ <string name="bad_config_error">%1$s в %2$s</string>
+ <string name="bad_config_explanation_pka">: Повинно бути додатним і не більше 65535</string>
+ <string name="bad_config_explanation_positive_number">: Повинно бути додатним</string>
+ <string name="bad_config_explanation_udp_port">: Повинен бути дійсним номером UDP порту</string>
+ <string name="bad_config_reason_invalid_key">Невірний ключ</string>
+ <string name="bad_config_reason_invalid_number">Недійсний номер</string>
+ <string name="bad_config_reason_invalid_value">Неприпустиме значення</string>
+ <string name="bad_config_reason_missing_attribute">Відсутній атрибут</string>
+ <string name="bad_config_reason_missing_section">Розділ відсутній</string>
+ <string name="bad_config_reason_syntax_error">Синтаксична помилка</string>
+ <string name="bad_config_reason_unknown_attribute">Невідомий атрибут</string>
+ <string name="bad_config_reason_unknown_section">Невідома секція</string>
+ <string name="bad_config_reason_value_out_of_range">Значення поза діапазоном</string>
+ <string name="bad_extension_error">Файл повинен мати розширення .conf або .zip</string>
+ <string name="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">Створити тунель WireGuard</string>
+ <string name="create_bin_dir_error">Не вдалося створити локальну бінарну теку</string>
+ <string name="create_downloads_file_error">Не вдалося створити файл в папці завантажень</string>
+ <string name="create_empty">Створити з нуля</string>
+ <string name="create_from_file">Імпортувати з файлу або архіву</string>
+ <string name="create_from_qr_code">Сканувати з QR-коду</string>
+ <string name="create_output_dir_error">Неможливо створити вихідний каталог</string>
+ <string name="create_temp_dir_error">Не вдалося створити локальну тимчасову папку</string>
+ <string name="create_tunnel">Створити тунель</string>
+ <string name="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">♥️ Пожертвуйте на проект 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>
+ <string name="generic_error">Невідома помилка “%s”</string>
+ <string name="hint_automatic">(авто)</string>
+ <string name="hint_generated">(згенеровано)</string>
+ <string name="hint_optional">(необов\'язково)</string>
+ <string name="hint_optional_discouraged">(необов\'язково, не рекомендується)</string>
+ <string name="hint_random">(випадковий)</string>
+ <string name="illegal_filename_error">Некоректна назва файлу“%s”</string>
+ <string name="import_error">Не вдалося імпортувати тунель: %s</string>
+ <string name="import_from_qr_code">Імпортувати тунель з QR-коду</string>
+ <string name="import_success">Імпортовано “%s”</string>
+ <string name="interface_title">Інтерфейс</string>
+ <string name="key_contents_error">Недопустимі символи в ключі</string>
+ <string name="key_length_error">Неправильна довжина ключа</string>
+ <string name="key_length_explanation_base64">: Ключі WireGuard base64 повинні мати довжину 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>
+ <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">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">керування тунелями WireGuard</string>
+ <string name="persistent_keepalive">Постійне з\'єднання</string>
+ <string name="pre_shared_key">Pre-shared ключ</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">Не вдалося запустити в оболонці: %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">Встановити додаткові інструменти для сценаріїв</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">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
+ <string name="tun_create_error">Не вдалося створити tun інтерфейс</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_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>
+ <string name="tunnel_rename_error">Не вдалося перейменувати тунель: %s</string>
+ <string name="tunnel_rename_success">Тунель успішно перейменовано на \"%s\"</string>
+ <string name="type_name_go_userspace">Go userspace</string>
+ <string name="type_name_kernel_module">Модуль ядра</string>
+ <string name="unknown_error">Невідома помилка</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>
+ <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-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
new file mode 100644
index 00000000..f94cadb1
--- /dev/null
+++ b/ui/src/main/res/values-v27/styles.xml
@@ -0,0 +1,10 @@
+<?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>
+ <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
new file mode 100644
index 00000000..7757e2fe
--- /dev/null
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,246 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="other">无法删除 %d 项:%s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="other">成功删除了 %d 项</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="other">已选择 %d 个隧道</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="other">导入了 %2$d 项中的 %1$d 项</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="other">导入了 %d 个隧道</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="other">对 %d 个应用不生效</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="other">对 %d 个应用生效</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="other">对 %d 个应用不生效</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <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="other">选定 %d 个应用</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="other">选定 %d 个应用</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="other">每隔 %d 秒</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="other">秒</item>
+ </plurals>
+ <string name="use_all_applications">不设定过滤</string>
+ <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_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_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">图片中未发现二维码</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_save_error">无法保存 “%1$s” 的配置:%2$s</string>
+ <string name="config_save_success">已保存 “%s” 的配置</string>
+ <string name="create_activity_title">创建 WireGuard 隧道</string>
+ <string name="create_bin_dir_error">无法创建本地二进制文件目录</string>
+ <string name="create_downloads_file_error">无法在下载目录中创建文件</string>
+ <string name="create_empty">手动创建</string>
+ <string name="create_from_file">导入配置或压缩包</string>
+ <string name="create_from_qr_code">扫描二维码</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">♥ 为 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>
+ <string name="generic_error">未知的 “%s” 错误</string>
+ <string name="hint_automatic">(自动)</string>
+ <string name="hint_generated">(生成)</string>
+ <string name="hint_optional">(可选)</string>
+ <string name="hint_optional_discouraged">(可选,不建议设置)</string>
+ <string name="hint_random">(随机)</string>
+ <string name="illegal_filename_error">文件名 “%s” 不合法</string>
+ <string name="import_error">无法导入隧道:%s</string>
+ <string name="import_from_qr_code">从二维码导入隧道</string>
+ <string name="import_success">导入了 “%s”</string>
+ <string name="interface_title">本地(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>
+ <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">\u0020IP 地址</string>
+ <string name="parse_error_inet_endpoint">对端</string>
+ <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="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="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">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">切换隧道状态时出错:%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">无法创建 tun 设备</string>
+ <string name="tunnel_config_error">无法配置隧道(wg-quick returned %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 returned %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 userspace</string>
+ <string name="type_name_kernel_module">Kernel module</string>
+ <string name="unknown_error">未知错误</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>
+ <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">导出隧道配置</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
new file mode 100644
index 00000000..14e83a20
--- /dev/null
+++ b/ui/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <plurals name="delete_error">
+ <item quantity="other">無法刪除 %d 個通道: %s</item>
+ </plurals>
+ <plurals name="delete_success">
+ <item quantity="other">成功刪除 %d 個通道</item>
+ </plurals>
+ <plurals name="delete_title">
+ <item quantity="other">已選擇 %d 個通道</item>
+ </plurals>
+ <plurals name="import_partial_success">
+ <item quantity="other">已匯入 %1$d 個通道 (共 %2$d 個)</item>
+ </plurals>
+ <plurals name="import_total_success">
+ <item quantity="other">已匯入 %d 個通道</item>
+ </plurals>
+ <plurals name="set_excluded_applications">
+ <item quantity="other">已排除 %d 個應用程式</item>
+ </plurals>
+ <plurals name="set_included_applications">
+ <item quantity="other">套用到 %d 個應用程式</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="other">已排除 %d</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <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="other">已套用 %d 個應用程式</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="other">已排除 %d 個應用程式</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="other">每隔 %d 秒</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <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">允許的 IPs</string>
+ <string name="bad_config_context">錯誤設定: %1$s 的 %2$s</string>
+ <string name="bad_config_context_top_level">頂層錯誤設定: %s</string>
+ <string name="bad_config_error">設定有誤: %1$s 在 %2$s</string>
+ <string name="bad_config_explanation_pka">: 必須是不大於 65535 的正整數</string>
+ <string name="bad_config_explanation_positive_number">: 必須是正整數</string>
+ <string name="bad_config_explanation_udp_port">: 必須是一個有效的 UDP 埠號</string>
+ <string name="bad_config_reason_invalid_key">無效的密鑰</string>
+ <string name="bad_config_reason_invalid_number">無效的號碼</string>
+ <string name="bad_config_reason_invalid_value">無效值</string>
+ <string name="bad_config_reason_missing_attribute">缺少屬性</string>
+ <string name="bad_config_reason_missing_section">缺少章節</string>
+ <string name="bad_config_reason_syntax_error">語法錯誤</string>
+ <string name="bad_config_reason_unknown_attribute">未知屬性</string>
+ <string name="bad_config_reason_unknown_section">未知章節</string>
+ <string name="bad_config_reason_value_out_of_range">內容值超出範圍</string>
+ <string name="bad_extension_error">必須是 .conf 或 .zip 的檔案</string>
+ <string name="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_save_error">無法儲存設定「%1$s」: %2$s</string>
+ <string name="config_save_success">成功儲存設定「%s」</string>
+ <string name="create_activity_title">新建 WireGuard 通道</string>
+ <string name="create_bin_dir_error">無法建立本地二進位目錄</string>
+ <string name="create_downloads_file_error">無法在下載目錄建立檔案</string>
+ <string name="create_empty">從空白開始建立</string>
+ <string name="create_from_file">從檔案或壓縮檔匯入</string>
+ <string name="create_from_qr_code">掃描 QR code</string>
+ <string name="create_output_dir_error">無法建立輸出目錄</string>
+ <string name="create_temp_dir_error">無法建立本地暫存目錄</string>
+ <string name="create_tunnel">新建通道</string>
+ <string name="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="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_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="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 Code 匯入通道</string>
+ <string name="import_success">已匯入「%s」</string>
+ <string name="interface_title">界面</string>
+ <string name="key_contents_error">密鑰包含錯誤的字元</string>
+ <string name="key_length_error">密鑰長度不正確</string>
+ <string name="key_length_explanation_base64">: 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_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">最大傳輸單元</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/app/src/main/res/values/attrs.xml b/ui/src/main/res/values/attrs.xml
index 86a86e63..b91ac60e 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/ui/src/main/res/values/attrs.xml
@@ -3,4 +3,8 @@
<declare-styleable name="Multiselected">
<attr name="state_multiselected" format="boolean" />
</declare-styleable>
+ <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/bools.xml b/ui/src/main/res/values/bools.xml
new file mode 100644
index 00000000..288f85a5
--- /dev/null
+++ b/ui/src/main/res/values/bools.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="light_status_bar">true</bool>
+ <bool name="light_navigation_bar">true</bool>
+</resources>
diff --git a/ui/src/main/res/values/colors.xml b/ui/src/main/res/values/colors.xml
new file mode 100644
index 00000000..65e82389
--- /dev/null
+++ b/ui/src/main/res/values/colors.xml
@@ -0,0 +1,63 @@
+<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
new file mode 100644
index 00000000..8ea07dfb
--- /dev/null
+++ b/ui/src/main/res/values/dimens.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="fab_margin">16dp</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/app/src/main/res/values/ic_launcher_background.xml b/ui/src/main/res/values/ic_launcher_background.xml
index f8bad52e..f8bad52e 100644
--- a/app/src/main/res/values/ic_launcher_background.xml
+++ b/ui/src/main/res/values/ic_launcher_background.xml
diff --git a/app/src/main/res/values/ids.xml b/ui/src/main/res/values/ids.xml
index 7f34f808..7f34f808 100644
--- a/app/src/main/res/values/ids.xml
+++ b/ui/src/main/res/values/ids.xml
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/app/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 33470706..df3d3340 100644
--- a/app/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -13,8 +13,8 @@
<item quantity="other">%d tunnels selected</item>
</plurals>
<plurals name="import_partial_success">
- <item quantity="one">Imported %d of %d tunnels</item>
- <item quantity="other">Imported %d of %d tunnels</item>
+ <item quantity="one">Imported %1$d of %2$d tunnels</item>
+ <item quantity="other">Imported %1$d of %2$d tunnels</item>
</plurals>
<plurals name="import_total_success">
<item quantity="one">Imported %d tunnel</item>
@@ -24,10 +24,46 @@
<item quantity="one">%d Excluded Application</item>
<item quantity="other">%d Excluded Applications</item>
</plurals>
+ <plurals name="set_included_applications">
+ <item quantity="one">%d Included Application</item>
+ <item quantity="other">%d Included Applications</item>
+ </plurals>
+ <plurals name="n_excluded_applications">
+ <item quantity="one">%d excluded</item>
+ <item quantity="other">%d excluded</item>
+ </plurals>
+ <plurals name="n_included_applications">
+ <item quantity="one">%d included</item>
+ <item quantity="other">%d included</item>
+ </plurals>
+ <string name="all_applications">All Applications</string>
+ <string name="exclude_from_tunnel">Exclude</string>
+ <string name="include_in_tunnel">Include only</string>
+ <plurals name="include_n_applications">
+ <item quantity="one">Include %d app</item>
+ <item quantity="other">Include %d apps</item>
+ </plurals>
+ <plurals name="exclude_n_applications">
+ <item quantity="one">Exclude %d app</item>
+ <item quantity="other">Exclude %d apps</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_unit">
+ <item quantity="one">every second</item>
+ <item quantity="other">every %d seconds</item>
+ </plurals>
+ <plurals name="persistent_keepalive_seconds_suffix">
+ <item quantity="one">second</item>
+ <item quantity="other">seconds</item>
+ </plurals>
+ <string name="use_all_applications">Use all apps</string>
<string name="add_peer">Add peer</string>
<string name="addresses">Addresses</string>
+ <string name="applications">Applications</string>
+ <string name="allow_remote_control_intents_summary_off">External apps may not toggle tunnels (recommended)</string>
+ <string name="allow_remote_control_intents_summary_on">External apps may toggle tunnels (advanced)</string>
+ <string name="allow_remote_control_intents_title">Allow remote control apps</string>
<string name="allowed_ips">Allowed IPs</string>
- <string name="app_name">WireGuard</string>
+ <string name="app_name" translatable="false">WireGuard</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s in %2$s</string>
@@ -39,12 +75,13 @@
<string name="bad_config_reason_invalid_value">Invalid value</string>
<string name="bad_config_reason_missing_attribute">Missing attribute</string>
<string name="bad_config_reason_missing_section">Missing section</string>
- <string name="bad_config_reason_missing_value">Missing value</string>
<string name="bad_config_reason_syntax_error">Syntax error</string>
<string name="bad_config_reason_unknown_attribute">Unknown attribute</string>
<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>
@@ -55,56 +92,80 @@
<string name="config_save_success">Successfully saved configuration for “%s”</string>
<string name="create_activity_title">Create WireGuard Tunnel</string>
<string name="create_bin_dir_error">Cannot create local binary directory</string>
+ <string name="create_downloads_file_error">Cannot create file in downloads directory</string>
<string name="create_empty">Create from scratch</string>
- <string name="create_from_file">Create from file or archive</string>
- <string name="create_from_qr_code">Create from QR code</string>
+ <string name="create_from_file">Import from file or archive</string>
+ <string name="create_from_qr_code">Scan from QR code</string>
<string name="create_output_dir_error">Cannot create output directory</string>
- <string name="create_downloads_file_error">Cannot create file in downloads directory</string>
<string name="create_temp_dir_error">Cannot create local temporary directory</string>
<string name="create_tunnel">Create Tunnel</string>
+ <string name="copied_to_clipboard">%s copied to clipboard</string>
<string name="dark_theme_summary_off">Currently using light (day) theme</string>
<string name="dark_theme_summary_on">Currently using dark (night) theme</string>
<string name="dark_theme_title">Use dark theme</string>
<string name="delete">Delete</string>
- <string name="deselect_all">Deselect All</string>
+ <string name="tv_delete">Select tunnel to delete</string>
+ <string name="tv_select_a_storage_drive">Select a storage drive</string>
+ <string name="tv_no_file_picker">Please install a file management utility to browse files</string>
+ <string name="tv_add_tunnel_get_started">Add a tunnel to get started</string>
+ <string name="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>
+ <string name="dns_search_domains">Search domains</string>
<string name="edit">Edit</string>
<string name="endpoint">Endpoint</string>
<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="excluded_applications">Excluded Applications</string>
- <string name="generate">Generate</string>
+ <string name="generate_new_private_key">Generate new private key</string>
<string name="generic_error">Unknown “%s” error</string>
<string name="hint_automatic">(auto)</string>
<string name="hint_generated">(generated)</string>
<string name="hint_optional">(optional)</string>
+ <string name="hint_optional_discouraged">(optional, not recommended)</string>
<string name="hint_random">(random)</string>
<string name="illegal_filename_error">Illegal file name “%s”</string>
<string name="import_error">Unable to import tunnel: %s</string>
<string name="import_from_qr_code">Import Tunnel from QR Code</string>
<string name="import_success">Imported “%s”</string>
<string name="interface_title">Interface</string>
+ <string name="key_contents_error">Bad characters in key</string>
+ <string name="key_length_error">Incorrect key length</string>
<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>
<string name="log_export_success">Saved to “%s”</string>
- <string name="log_export_summary">Log file will be saved to downloads folder</string>
<string name="log_export_title">Export log file</string>
+ <string name="log_saver_activity_label">Save log</string>
+ <string name="log_viewer_pref_summary">Logs may assist with debugging</string>
+ <string name="log_viewer_pref_title">View application log</string>
+ <string name="log_viewer_title">Log</string>
<string name="logcat_error">Unable to run logcat: </string>
- <string name="module_version_error">Unable to determine kernel module version</string>
- <string name="module_installer_not_found">No modules are available for your device</string>
+ <string name="module_enabler_disabled_summary">The experimental kernel module can improve performance</string>
+ <string name="module_enabler_disabled_title">Enable kernel module backend</string>
+ <string name="module_enabler_enabled_summary">The slower userspace backend may improve stability</string>
+ <string name="module_enabler_enabled_title">Disable kernel module backend</string>
+ <string name="module_installer_error">Something went wrong. Please try again</string>
<string name="module_installer_initial">The experimental kernel module can improve performance</string>
- <string name="module_installer_success">Success. The application will restart in 5 seconds</string>
+ <string name="module_installer_not_found">No modules are available for your device</string>
<string name="module_installer_title">Download and install kernel module</string>
<string name="module_installer_working">Downloading and installing…</string>
- <string name="module_installer_error">Something went wrong. Please try again</string>
+ <string name="module_version_error">Unable to determine kernel module version</string>
<string name="mtu">MTU</string>
- <string name="multiple_tunnels_error">Only one userspace tunnel can run at a time</string>
+ <string name="multiple_tunnels_summary_off">Turning on one tunnel will turn off others</string>
+ <string name="multiple_tunnels_summary_on">Multiple tunnels may be turned on simultaneously</string>
+ <string name="multiple_tunnels_title">Allow multiple simultaneous tunnels</string>
<string name="name">Name</string>
<string name="no_config_error">Trying to bring up a tunnel with no config</string>
<string name="no_configs_error">No configurations found</string>
@@ -116,23 +177,29 @@
<string name="parse_error_integer">number</string>
<string name="parse_error_reason">Cannot parse %1$s “%2$s”</string>
<string name="peer">Peer</string>
- <string name="permission_description">Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.</string>
+ <string name="permission_description">control WireGuard tunnels, enabling and disabling tunnels at will, potentially misdirecting Internet traffic</string>
<string name="permission_label">control WireGuard tunnels</string>
<string name="persistent_keepalive">Persistent keepalive</string>
<string name="pre_shared_key">Pre-shared key</string>
+ <string name="pre_shared_key_enabled">enabled</string>
<string name="private_key">Private key</string>
<string name="public_key">Public key</string>
- <string name="public_key_description">Public key</string>
<string name="qr_code_hint">Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
- <string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</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>
<string name="save">Save</string>
<string name="select_all">Select all</string>
- <string name="set_exclusions">Set Exclusions</string>
<string name="settings">Settings</string>
<string name="shell_exit_status_read_error">Shell cannot read exit status</string>
<string name="shell_marker_count_error">Shell expected 4 markers, received %d</string>
<string name="shell_start_error">Shell failed to start: %d</string>
+ <string name="success_application_will_restart">Success. The application will now restart…</string>
+ <string name="toggle_all">Toggle All</string>
<string name="toggle_error">Error toggling WireGuard tunnel: %s</string>
<string name="tools_installer_already">wg and wg-quick are already installed</string>
<string name="tools_installer_failure">Unable to install command-line tools (no root?)</string>
@@ -144,21 +211,39 @@
<string name="tools_installer_title">Install command line tools</string>
<string name="tools_installer_working">Installing wg and wg-quick</string>
<string name="tools_unavailable_error">Required tools unavailable</string>
+ <string name="transfer">Transfer</string>
+ <string name="transfer_bytes">%d B</string>
+ <string name="transfer_gibibytes">%.2f GiB</string>
+ <string name="transfer_kibibytes">%.2f KiB</string>
+ <string name="transfer_mibibytes">%.2f MiB</string>
+ <string name="transfer_rx_tx">rx: %1$s, tx: %2$s</string>
+ <string name="transfer_tibibytes">%.2f TiB</string>
<string name="tun_create_error">Unable to create tun device</string>
<string name="tunnel_config_error">Unable to configure tunnel (wg-quick returned %d)</string>
<string name="tunnel_create_error">Unable to create tunnel: %s</string>
<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>
<string name="tunnel_rename_error">Unable to rename tunnel: %s</string>
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
<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="version_summary">%1$s backend v%2$s</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>
<string name="version_title">WireGuard for Android v%s</string>
@@ -168,6 +253,8 @@
<string name="zip_export_success">Saved to “%s”</string>
<string name="zip_export_summary">Zip file will be saved to downloads folder</string>
<string name="zip_export_title">Export tunnels to zip file</string>
- <string name="key_length_error">Incorrect key length</string>
- <string name="key_contents_error">Bad characters in key</string>
+ <string name="biometric_prompt_zip_exporter_title">Authenticate to export tunnels</string>
+ <string name="biometric_prompt_private_key_title">Authenticate to view private key</string>
+ <string name="biometric_auth_error">Authentication failure</string>
+ <string name="biometric_auth_error_reason">Authentication failure: %s</string>
</resources>
diff --git a/ui/src/main/res/values/styles.xml b/ui/src/main/res/values/styles.xml
new file mode 100644
index 00000000..39c38247
--- /dev/null
+++ b/ui/src/main/res/values/styles.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="WireGuardTheme.Toolbar" parent="Widget.Material3.Toolbar">
+ <item name="android:background">?attr/colorSurface</item>
+ </style>
+
+ <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="WireGuardTheme.MaterialCardView" parent="Widget.Material3.CardView.Elevated">
+ <item name="cornerRadius">4dp</item>
+ <item name="contentPadding">8dp</item>
+ </style>
+
+ <style name="WireGuardTheme.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ </style>
+
+ <style name="NoBackgroundTheme" parent="AppTheme">
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="colorPrimaryDark">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:windowEnterAnimation">@android:anim/fade_in</item>
+ <item name="android:windowExitAnimation">@android:anim/fade_out</item>
+ </style>
+
+ <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/xml/app_restrictions.xml b/ui/src/main/res/xml/app_restrictions.xml
new file mode 100644
index 00000000..2eaa7bc5
--- /dev/null
+++ b/ui/src/main/res/xml/app_restrictions.xml
@@ -0,0 +1,13 @@
+<!--
+ ~ Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
+ <restriction
+ android:defaultValue="false"
+ android:description="@string/disable_config_export_description"
+ android:key="disable_config_export"
+ android:restrictionType="bool"
+ android:title="@string/disable_config_export_title" />
+</restrictions>
diff --git a/ui/src/main/res/xml/preferences.xml b/ui/src/main/res/xml/preferences.xml
new file mode 100644
index 00000000..a8b66df7
--- /dev/null
+++ b/ui/src/main/res/xml/preferences.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:key="settings">
+ <com.wireguard.android.preference.VersionPreference
+ android:icon="@mipmap/ic_launcher"
+ android:key="version" />
+ <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" />
+ <com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
+ <com.wireguard.android.preference.KernelModuleEnablerPreference android:key="kernel_module_enabler" />
+ <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>