From 7db0fa915ef909ad33b9b29754e1fd1e7ed260a9 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 5 Apr 2020 21:37:45 -0600 Subject: AppListDialogFragment: support both inclusion and exclusion Signed-off-by: Jason A. Donenfeld --- .../android/fragment/AppListDialogFragment.kt | 77 +++++++++++++++------- .../android/fragment/TunnelEditorFragment.kt | 35 +++++++--- .../com/wireguard/android/model/ApplicationData.kt | 6 +- .../wireguard/android/viewmodel/InterfaceProxy.kt | 7 ++ .../main/res/layout/app_list_dialog_fragment.xml | 60 +++++++++++------ ui/src/main/res/layout/app_list_item.xml | 6 +- ui/src/main/res/layout/tunnel_editor_fragment.xml | 4 +- ui/src/main/res/layout/tunnel_editor_peer.xml | 2 +- ui/src/main/res/values-de/strings.xml | 2 - ui/src/main/res/values-hi/strings.xml | 2 - ui/src/main/res/values-id/strings.xml | 2 - ui/src/main/res/values-it/strings.xml | 2 - ui/src/main/res/values-ja/strings.xml | 2 - ui/src/main/res/values-ru/strings.xml | 2 - ui/src/main/res/values-sl/strings.xml | 4 +- ui/src/main/res/values-zh-rCN/strings.xml | 2 - ui/src/main/res/values/strings.xml | 18 ++++- 17 files changed, 152 insertions(+), 81 deletions(-) (limited to 'ui') diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt index 8bf0cf14..4fdebd16 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt @@ -5,14 +5,17 @@ package com.wireguard.android.fragment import android.app.Dialog -import android.content.DialogInterface import android.content.Intent import android.os.Bundle +import android.widget.Button import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.databinding.Observable import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import com.google.android.material.tabs.TabLayout import com.wireguard.android.Application +import com.wireguard.android.BR import com.wireguard.android.R import com.wireguard.android.databinding.AppListDialogFragmentBinding import com.wireguard.android.databinding.ObservableKeyedArrayList @@ -21,7 +24,10 @@ import com.wireguard.android.util.ErrorMessages class AppListDialogFragment : DialogFragment() { private val appData: ObservableKeyedArrayList = ObservableKeyedArrayList() - private var currentlyExcludedApps = emptyList() + private var currentlySelectedApps = emptyList() + private var initiallyExcluded: Boolean = false + private var button: Button? = null + private var tabs: TabLayout? = null private fun loadData() { val activity = activity ?: return @@ -33,7 +39,14 @@ class AppListDialogFragment : DialogFragment() { val applicationData: MutableList = ArrayList() resolveInfos.forEach { val packageName = it.activityInfo.packageName - applicationData.add(ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName))) + val appData = ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName)) + applicationData.add(appData) + appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + if (propertyId == BR.selected) + setButtonText() + } + }) } applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) applicationData @@ -52,17 +65,34 @@ class AppListDialogFragment : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS) - currentlyExcludedApps = (excludedApps ?: emptyList()) + currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList()) + initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true + } + + 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 = AlertDialog.Builder(requireActivity()) - alertDialogBuilder.setTitle(R.string.excluded_applications) val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false) binding.executePendingBindings() alertDialogBuilder.setView(binding.root) - alertDialogBuilder.setPositiveButton(R.string.set_exclusions) { _, _ -> setExclusionsAndDismiss() } + tabs = binding.tabs + tabs!!.selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1)) + tabs!!.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 @@ -70,39 +100,40 @@ class AppListDialogFragment : DialogFragment() { loadData() val dialog = alertDialogBuilder.create() dialog.setOnShowListener { - dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener { - val selectedItems = appData - .filter { it.isExcludedFromTunnel } - - val excludeAll = selectedItems.isEmpty() + button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + setButtonText() + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ -> + val selectAll = appData.none { it.isSelected } appData.forEach { - it.isExcludedFromTunnel = excludeAll + it.isSelected = selectAll } } } return dialog } - private fun setExclusionsAndDismiss() { - val excludedApps: MutableList = ArrayList() + private fun setSelectionAndDismiss() { + val selectedApps: MutableList = ArrayList() for (data in appData) { - if (data.isExcludedFromTunnel) { - excludedApps.add(data.packageName) + if (data.isSelected) { + selectedApps.add(data.packageName) } } - (targetFragment as AppExclusionListener?)!!.onExcludedAppsSelected(excludedApps) + (targetFragment as AppSelectionListener?)!!.onSelectedAppsSelected(selectedApps, tabs?.selectedTabPosition == 0) dismiss() } - interface AppExclusionListener { - fun onExcludedAppsSelected(excludedApps: List) + interface AppSelectionListener { + fun onSelectedAppsSelected(selectedApps: List, isExcluded: Boolean) } companion object { - private const val KEY_EXCLUDED_APPS = "excludedApps" - fun newInstance(excludedApps: ArrayList?, target: T): AppListDialogFragment where T : Fragment?, T : AppExclusionListener? { + private const val KEY_SELECTED_APPS = "selected_apps" + private const val KEY_IS_EXCLUDED = "is_excluded" + fun newInstance(selectedApps: ArrayList?, isExcluded: Boolean, target: T): AppListDialogFragment where T : Fragment?, T : AppSelectionListener? { val extras = Bundle() - extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps) + extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps) + extras.putBoolean(KEY_IS_EXCLUDED, isExcluded) val fragment = AppListDialogFragment() fragment.setTargetFragment(target, 0) fragment.arguments = extras diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt index 2e1e4531..dc1b8aa2 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -23,7 +23,7 @@ import com.wireguard.android.Application import com.wireguard.android.R import com.wireguard.android.backend.Tunnel import com.wireguard.android.databinding.TunnelEditorFragmentBinding -import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener +import com.wireguard.android.fragment.AppListDialogFragment.AppSelectionListener import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.BiometricAuthenticator import com.wireguard.android.util.ErrorMessages @@ -35,7 +35,7 @@ import com.wireguard.config.Config /** * Fragment for editing a WireGuard configuration. */ -class TunnelEditorFragment : BaseFragment(), AppExclusionListener { +class TunnelEditorFragment : BaseFragment(), AppSelectionListener { private var haveShownKeys = false private var binding: TunnelEditorFragmentBinding? = null private var tunnel: ObservableTunnel? = null @@ -88,11 +88,20 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener { super.onDestroyView() } - override fun onExcludedAppsSelected(excludedApps: List) { - requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" } - binding!!.config!!.`interface`.excludedApplications.apply { - clear() - addAll(excludedApps) + override fun onSelectedAppsSelected(selectedApps: List, isExcluded: Boolean) { + requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" } + if (isExcluded) { + binding!!.config!!.`interface`.includedApplications.clear() + binding!!.config!!.`interface`.excludedApplications.apply { + clear() + addAll(selectedApps) + } + } else { + binding!!.config!!.`interface`.excludedApplications.clear() + binding!!.config!!.`interface`.includedApplications.apply { + clear() + addAll(selectedApps) + } } } @@ -150,10 +159,16 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener { } @Suppress("UNUSED_PARAMETER") - fun onRequestSetExcludedApplications(view: View?) { + fun onRequestSetExcludedIncludedApplications(view: View?) { if (binding != null) { - val excludedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications) - val fragment = AppListDialogFragment.newInstance(excludedApps, this) + 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, this) fragment.show(parentFragmentManager, null) } } diff --git a/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt b/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt index e931cdd2..e0961f04 100644 --- a/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt +++ b/ui/src/main/java/com/wireguard/android/model/ApplicationData.kt @@ -10,13 +10,13 @@ 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, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed { +class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed { override val key = name @get:Bindable - var isExcludedFromTunnel = isExcludedFromTunnel + var isSelected = isSelected set(value) { field = value - notifyPropertyChanged(BR.excludedFromTunnel) + notifyPropertyChanged(BR.selected) } } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt index 53f2fa24..bd2a9831 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt @@ -22,6 +22,9 @@ class InterfaceProxy : BaseObservable, Parcelable { @get:Bindable val excludedApplications: ObservableList = ObservableArrayList() + @get:Bindable + val includedApplications: ObservableList = ObservableArrayList() + @get:Bindable var addresses: String = "" set(value) { @@ -70,6 +73,7 @@ class InterfaceProxy : BaseObservable, Parcelable { addresses = parcel.readString() ?: "" dnsServers = parcel.readString() ?: "" parcel.readStringList(excludedApplications) + parcel.readStringList(includedApplications) listenPort = parcel.readString() ?: "" mtu = parcel.readString() ?: "" privateKey = parcel.readString() ?: "" @@ -80,6 +84,7 @@ class InterfaceProxy : BaseObservable, Parcelable { val dnsServerStrings = other.dnsServers.map { it.hostAddress } 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 @@ -103,6 +108,7 @@ class InterfaceProxy : BaseObservable, Parcelable { 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) @@ -113,6 +119,7 @@ class InterfaceProxy : BaseObservable, Parcelable { dest.writeString(addresses) dest.writeString(dnsServers) dest.writeStringList(excludedApplications) + dest.writeStringList(includedApplications) dest.writeString(listenPort) dest.writeString(mtu) dest.writeString(privateKey) diff --git a/ui/src/main/res/layout/app_list_dialog_fragment.xml b/ui/src/main/res/layout/app_list_dialog_fragment.xml index 7a9b1eb1..4503de15 100644 --- a/ui/src/main/res/layout/app_list_dialog_fragment.xml +++ b/ui/src/main/res/layout/app_list_dialog_fragment.xml @@ -18,30 +18,50 @@ type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" /> - - - - - + + + android:layout_height="wrap_content"> + + - + + + + + + + + diff --git a/ui/src/main/res/layout/app_list_item.xml b/ui/src/main/res/layout/app_list_item.xml index 5ce7a8a5..e4e4483c 100644 --- a/ui/src/main/res/layout/app_list_item.xml +++ b/ui/src/main/res/layout/app_list_item.xml @@ -24,7 +24,7 @@ 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:paddingTop="8dp" android:paddingBottom="8dp"> @@ -51,10 +51,10 @@ tools:text="@tools:sample/full_names" /> diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml index d5724c11..5f84e5f7 100644 --- a/ui/src/main/res/layout/tunnel_editor_fragment.xml +++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml @@ -220,8 +220,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" - android:onClick="@{fragment::onRequestSetExcludedApplications}" - android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}" + 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" diff --git a/ui/src/main/res/layout/tunnel_editor_peer.xml b/ui/src/main/res/layout/tunnel_editor_peer.xml index f00a6d26..d17378f2 100644 --- a/ui/src/main/res/layout/tunnel_editor_peer.xml +++ b/ui/src/main/res/layout/tunnel_editor_peer.xml @@ -159,7 +159,7 @@ Bitte root-Zugriff anfordern und erneut versuchen Fehler beim Starten des Tunnels: %s Private IPs ausschließen - Ausgeschlossene Anwendungen Neuen privaten Schlüssel generieren Unbekannter „%s“ Fehler (auto) @@ -142,7 +141,6 @@ Beim Neustart wiederherstellen Speichern Alle auswählen - Ausnahmen festlegen Einstellungen Shell kann den Exit-Status nicht lesen Die Shell erwartete 4 Marker, erhielt aber %d diff --git a/ui/src/main/res/values-hi/strings.xml b/ui/src/main/res/values-hi/strings.xml index 69aff26b..19d970b8 100644 --- a/ui/src/main/res/values-hi/strings.xml +++ b/ui/src/main/res/values-hi/strings.xml @@ -72,7 +72,6 @@ कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें टनल को लाने में त्रुटि: %s निजी आईपी को छोड़ दें - निकाले गए ऐप्स अज्ञात “%s” त्रुटि (ऑटो) (उत्पन्न) @@ -128,7 +127,6 @@ बूट पर पुनर्स्थापित करें सहेजें सभी का चयन करे - बहिष्करण सेट करें सेटिंग्स शेल बाहर निकलने की स्थिति नहीं पढ़ सकता शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया diff --git a/ui/src/main/res/values-id/strings.xml b/ui/src/main/res/values-id/strings.xml index ba45ea22..ca3ba316 100644 --- a/ui/src/main/res/values-id/strings.xml +++ b/ui/src/main/res/values-id/strings.xml @@ -70,7 +70,6 @@ Izinkan akses root dan coba lagi Kesalahan menambahkan tunel: %s Kecualikan IP pribadi - Kecualikan aplikasi Buat kunci privat baru Eror “%s” Tidak diketahui (otomatis) @@ -136,7 +135,6 @@ Pulihkan saat boot Simpan Pilih semua - Tetapkan pengecualian Pengaturan Shell tidak dapat membaca status keluar Shell diharapkan 4 nilai, diterima %d diff --git a/ui/src/main/res/values-it/strings.xml b/ui/src/main/res/values-it/strings.xml index eee91af1..0abd3c3a 100644 --- a/ui/src/main/res/values-it/strings.xml +++ b/ui/src/main/res/values-it/strings.xml @@ -76,7 +76,6 @@ Accedi come root e riprova Errore di attivazione del tunnel: %s Escludi IP privati - Applicazioni escluse Genera nuova chiave privata Errore “%s” sconosciuto (auto) @@ -142,7 +141,6 @@ Ripristina all\'avvio Salva Seleziona tutto - Imposta esclusioni Impostazioni La shell non riesce a leggere lo stato di uscita La shell si aspettava 4 marker, ne ha ricevuti %d diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml index f0ddc23d..f8054ab2 100644 --- a/ui/src/main/res/values-ja/strings.xml +++ b/ui/src/main/res/values-ja/strings.xml @@ -70,7 +70,6 @@ root 権限を取得して再試行してください トンネル起動時エラー: %s プライベート IP アドレスを除外 - 対象外とするアプリケーション 新しい秘密鍵を生成する 未知の “%s” エラー (自動) @@ -136,7 +135,6 @@ 起動時に復元 保存 すべて選択 - 対象外アプリを設定 設定 シェルは終了ステータスを取得できません シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml index 970176c2..5f9927a0 100644 --- a/ui/src/main/res/values-ru/strings.xml +++ b/ui/src/main/res/values-ru/strings.xml @@ -88,7 +88,6 @@ Пожалуйста, получите root-доступ и попробуйте снова Ошибка при запуске туннеля: %s Исключить частные IP-адреса - Исключенные приложения Сгенерировать новый приватный ключ Неизвестная “%s” ошибка (авто) @@ -154,7 +153,6 @@ Восстанавливать при загрузке Сохранить Выбрать все - ОК Настройки Shell не может прочитать статус выхода Shell ожидает 4 маркера, получено %d diff --git a/ui/src/main/res/values-sl/strings.xml b/ui/src/main/res/values-sl/strings.xml index d0ad8316..4aa1dce6 100644 --- a/ui/src/main/res/values-sl/strings.xml +++ b/ui/src/main/res/values-sl/strings.xml @@ -65,7 +65,7 @@ Konfiguracijska datoteka za „%s“ že obstaja Konfiguracijske datoteke za „%s“ ni bilo mogoče najti Konfiguracijske datoteke za „%s“ ni bilo mogoče preimenovati - Konfiguracijske datoteke za „%s“ ni bilo mogoče shraniti: %2$s + Konfiguracijske datoteke za „%1$s“ ni bilo mogoče shraniti: %2$s Konfiguracijska datoteka za „%s“ uspešno shranjena Ustvarite WireGuard tunel Lokalnega imenika za aplikacijo ni bilo mogoče kreirati @@ -88,7 +88,6 @@ Prosim omogočite root dostop in poskusite ponovno Napaka pri vzpostavitvi tunela: %s Izključitev privatnih IP naslovov - Izključitev aplikacije Generiraj nov privatni ključ Neznana napaka „%s“ (samodejno) @@ -154,7 +153,6 @@ Ponovna vzpostavitev pri ponovnem zagonu Shrani Izbor vseh - Določite izključitve Nastavitve Lupina ne more prebrati izhodnega statusa Lupina je pričakovala 4 markerje, dobila pa je %d diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml index 45d20603..ca4e8fcd 100644 --- a/ui/src/main/res/values-zh-rCN/strings.xml +++ b/ui/src/main/res/values-zh-rCN/strings.xml @@ -70,7 +70,6 @@ 请获取 root 权限并重试 建立连接时出错:%s 排除局域网 - 排除的应用 生成新的私钥 未知的 “%s” 错误 (自动) @@ -136,7 +135,6 @@ 启动时恢复 保存 全选 - 确定 设置 Shell 无法读取退出状态 Shell 应获取 4 个标记,获取到 %d 个 diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 50bc40b8..da0f0be7 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -24,6 +24,22 @@ %d Excluded Application %d Excluded Applications + + %d Included Application + %d Included Applications + + All Applications + Exclude: + Include only: + + Include %d apps + Include %d apps + + + Exclude %d apps + Exclude %d apps + + Use all apps Add peer Addresses External apps may not toggle tunnels (recommended) @@ -76,7 +92,6 @@ Please obtain root access and try again Error bringing up tunnel: %s Exclude private IPs - Excluded Applications Generate new private key Unknown “%s” error (auto) @@ -142,7 +157,6 @@ Restore on boot Save Select all - Set Exclusions Settings Shell cannot read exit status Shell expected 4 markers, received %d -- cgit v1.2.3