Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>master
@@ -5,14 +5,17 @@ | |||||
package com.wireguard.android.fragment | package com.wireguard.android.fragment | ||||
import android.app.Dialog | import android.app.Dialog | ||||
import android.content.DialogInterface | |||||
import android.content.Intent | import android.content.Intent | ||||
import android.os.Bundle | import android.os.Bundle | ||||
import android.widget.Button | |||||
import android.widget.Toast | import android.widget.Toast | ||||
import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||
import androidx.databinding.Observable | |||||
import androidx.fragment.app.DialogFragment | import androidx.fragment.app.DialogFragment | ||||
import androidx.fragment.app.Fragment | import androidx.fragment.app.Fragment | ||||
import com.google.android.material.tabs.TabLayout | |||||
import com.wireguard.android.Application | import com.wireguard.android.Application | ||||
import com.wireguard.android.BR | |||||
import com.wireguard.android.R | import com.wireguard.android.R | ||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding | import com.wireguard.android.databinding.AppListDialogFragmentBinding | ||||
import com.wireguard.android.databinding.ObservableKeyedArrayList | import com.wireguard.android.databinding.ObservableKeyedArrayList | ||||
@@ -21,7 +24,10 @@ import com.wireguard.android.util.ErrorMessages | |||||
class AppListDialogFragment : DialogFragment() { | class AppListDialogFragment : DialogFragment() { | ||||
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList() | private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList() | ||||
private var currentlyExcludedApps = emptyList<String>() | |||||
private var currentlySelectedApps = emptyList<String>() | |||||
private var initiallyExcluded: Boolean = false | |||||
private var button: Button? = null | |||||
private var tabs: TabLayout? = null | |||||
private fun loadData() { | private fun loadData() { | ||||
val activity = activity ?: return | val activity = activity ?: return | ||||
@@ -33,7 +39,14 @@ class AppListDialogFragment : DialogFragment() { | |||||
val applicationData: MutableList<ApplicationData> = ArrayList() | val applicationData: MutableList<ApplicationData> = ArrayList() | ||||
resolveInfos.forEach { | resolveInfos.forEach { | ||||
val packageName = it.activityInfo.packageName | 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.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
applicationData | applicationData | ||||
@@ -52,17 +65,34 @@ class AppListDialogFragment : DialogFragment() { | |||||
override fun onCreate(savedInstanceState: Bundle?) { | override fun onCreate(savedInstanceState: Bundle?) { | ||||
super.onCreate(savedInstanceState) | 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 { | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||||
val alertDialogBuilder = AlertDialog.Builder(requireActivity()) | val alertDialogBuilder = AlertDialog.Builder(requireActivity()) | ||||
alertDialogBuilder.setTitle(R.string.excluded_applications) | |||||
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false) | val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false) | ||||
binding.executePendingBindings() | binding.executePendingBindings() | ||||
alertDialogBuilder.setView(binding.root) | 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.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } | ||||
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> } | alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> } | ||||
binding.fragment = this | binding.fragment = this | ||||
@@ -70,39 +100,40 @@ class AppListDialogFragment : DialogFragment() { | |||||
loadData() | loadData() | ||||
val dialog = alertDialogBuilder.create() | val dialog = alertDialogBuilder.create() | ||||
dialog.setOnShowListener { | 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 { | appData.forEach { | ||||
it.isExcludedFromTunnel = excludeAll | |||||
it.isSelected = selectAll | |||||
} | } | ||||
} | } | ||||
} | } | ||||
return dialog | return dialog | ||||
} | } | ||||
private fun setExclusionsAndDismiss() { | |||||
val excludedApps: MutableList<String> = ArrayList() | |||||
private fun setSelectionAndDismiss() { | |||||
val selectedApps: MutableList<String> = ArrayList() | |||||
for (data in appData) { | 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() | dismiss() | ||||
} | } | ||||
interface AppExclusionListener { | |||||
fun onExcludedAppsSelected(excludedApps: List<String>) | |||||
interface AppSelectionListener { | |||||
fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean) | |||||
} | } | ||||
companion object { | companion object { | ||||
private const val KEY_EXCLUDED_APPS = "excludedApps" | |||||
fun <T> newInstance(excludedApps: ArrayList<String?>?, 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 <T> newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean, target: T): AppListDialogFragment where T : Fragment?, T : AppSelectionListener? { | |||||
val extras = Bundle() | val extras = Bundle() | ||||
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps) | |||||
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps) | |||||
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded) | |||||
val fragment = AppListDialogFragment() | val fragment = AppListDialogFragment() | ||||
fragment.setTargetFragment(target, 0) | fragment.setTargetFragment(target, 0) | ||||
fragment.arguments = extras | fragment.arguments = extras | ||||
@@ -23,7 +23,7 @@ import com.wireguard.android.Application | |||||
import com.wireguard.android.R | import com.wireguard.android.R | ||||
import com.wireguard.android.backend.Tunnel | import com.wireguard.android.backend.Tunnel | ||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding | 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.model.ObservableTunnel | ||||
import com.wireguard.android.util.BiometricAuthenticator | import com.wireguard.android.util.BiometricAuthenticator | ||||
import com.wireguard.android.util.ErrorMessages | import com.wireguard.android.util.ErrorMessages | ||||
@@ -35,7 +35,7 @@ import com.wireguard.config.Config | |||||
/** | /** | ||||
* Fragment for editing a WireGuard configuration. | * Fragment for editing a WireGuard configuration. | ||||
*/ | */ | ||||
class TunnelEditorFragment : BaseFragment(), AppExclusionListener { | |||||
class TunnelEditorFragment : BaseFragment(), AppSelectionListener { | |||||
private var haveShownKeys = false | private var haveShownKeys = false | ||||
private var binding: TunnelEditorFragmentBinding? = null | private var binding: TunnelEditorFragmentBinding? = null | ||||
private var tunnel: ObservableTunnel? = null | private var tunnel: ObservableTunnel? = null | ||||
@@ -88,11 +88,20 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener { | |||||
super.onDestroyView() | super.onDestroyView() | ||||
} | } | ||||
override fun onExcludedAppsSelected(excludedApps: List<String>) { | |||||
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<String>, 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") | @Suppress("UNUSED_PARAMETER") | ||||
fun onRequestSetExcludedApplications(view: View?) { | |||||
fun onRequestSetExcludedIncludedApplications(view: View?) { | |||||
if (binding != null) { | 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) | fragment.show(parentFragmentManager, null) | ||||
} | } | ||||
} | } | ||||
@@ -10,13 +10,13 @@ import androidx.databinding.Bindable | |||||
import com.wireguard.android.BR | import com.wireguard.android.BR | ||||
import com.wireguard.android.databinding.Keyed | import com.wireguard.android.databinding.Keyed | ||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed<String> { | |||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> { | |||||
override val key = name | override val key = name | ||||
@get:Bindable | @get:Bindable | ||||
var isExcludedFromTunnel = isExcludedFromTunnel | |||||
var isSelected = isSelected | |||||
set(value) { | set(value) { | ||||
field = value | field = value | ||||
notifyPropertyChanged(BR.excludedFromTunnel) | |||||
notifyPropertyChanged(BR.selected) | |||||
} | } | ||||
} | } |
@@ -22,6 +22,9 @@ class InterfaceProxy : BaseObservable, Parcelable { | |||||
@get:Bindable | @get:Bindable | ||||
val excludedApplications: ObservableList<String> = ObservableArrayList() | val excludedApplications: ObservableList<String> = ObservableArrayList() | ||||
@get:Bindable | |||||
val includedApplications: ObservableList<String> = ObservableArrayList() | |||||
@get:Bindable | @get:Bindable | ||||
var addresses: String = "" | var addresses: String = "" | ||||
set(value) { | set(value) { | ||||
@@ -70,6 +73,7 @@ class InterfaceProxy : BaseObservable, Parcelable { | |||||
addresses = parcel.readString() ?: "" | addresses = parcel.readString() ?: "" | ||||
dnsServers = parcel.readString() ?: "" | dnsServers = parcel.readString() ?: "" | ||||
parcel.readStringList(excludedApplications) | parcel.readStringList(excludedApplications) | ||||
parcel.readStringList(includedApplications) | |||||
listenPort = parcel.readString() ?: "" | listenPort = parcel.readString() ?: "" | ||||
mtu = parcel.readString() ?: "" | mtu = parcel.readString() ?: "" | ||||
privateKey = parcel.readString() ?: "" | privateKey = parcel.readString() ?: "" | ||||
@@ -80,6 +84,7 @@ class InterfaceProxy : BaseObservable, Parcelable { | |||||
val dnsServerStrings = other.dnsServers.map { it.hostAddress } | val dnsServerStrings = other.dnsServers.map { it.hostAddress } | ||||
dnsServers = Attribute.join(dnsServerStrings) | dnsServers = Attribute.join(dnsServerStrings) | ||||
excludedApplications.addAll(other.excludedApplications) | excludedApplications.addAll(other.excludedApplications) | ||||
includedApplications.addAll(other.includedApplications) | |||||
listenPort = other.listenPort.map { it.toString() }.orElse("") | listenPort = other.listenPort.map { it.toString() }.orElse("") | ||||
mtu = other.mtu.map { it.toString() }.orElse("") | mtu = other.mtu.map { it.toString() }.orElse("") | ||||
val keyPair = other.keyPair | val keyPair = other.keyPair | ||||
@@ -103,6 +108,7 @@ class InterfaceProxy : BaseObservable, Parcelable { | |||||
if (addresses.isNotEmpty()) builder.parseAddresses(addresses) | if (addresses.isNotEmpty()) builder.parseAddresses(addresses) | ||||
if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers) | if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers) | ||||
if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications) | if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications) | ||||
if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications) | |||||
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort) | if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort) | ||||
if (mtu.isNotEmpty()) builder.parseMtu(mtu) | if (mtu.isNotEmpty()) builder.parseMtu(mtu) | ||||
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey) | if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey) | ||||
@@ -113,6 +119,7 @@ class InterfaceProxy : BaseObservable, Parcelable { | |||||
dest.writeString(addresses) | dest.writeString(addresses) | ||||
dest.writeString(dnsServers) | dest.writeString(dnsServers) | ||||
dest.writeStringList(excludedApplications) | dest.writeStringList(excludedApplications) | ||||
dest.writeStringList(includedApplications) | |||||
dest.writeString(listenPort) | dest.writeString(listenPort) | ||||
dest.writeString(mtu) | dest.writeString(mtu) | ||||
dest.writeString(privateKey) | dest.writeString(privateKey) | ||||
@@ -18,30 +18,50 @@ | |||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" /> | type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" /> | ||||
</data> | </data> | ||||
<FrameLayout | |||||
<LinearLayout | |||||
android:layout_width="match_parent" | android:layout_width="match_parent" | ||||
android:layout_height="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:orientation="vertical"> | |||||
<com.google.android.material.tabs.TabLayout | |||||
android:id="@+id/tabs" | |||||
style="@style/Widget.MaterialComponents.TabLayout.Colored" | |||||
android:layout_width="match_parent" | 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" /> | |||||
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" /> | |||||
</FrameLayout> | |||||
<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> | </layout> |
@@ -24,7 +24,7 @@ | |||||
android:layout_height="wrap_content" | android:layout_height="wrap_content" | ||||
android:background="@drawable/list_item_background" | android:background="@drawable/list_item_background" | ||||
android:gravity="center_vertical" | android:gravity="center_vertical" | ||||
android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}" | |||||
android:onClick="@{(view) -> item.setSelected(!item.selected)}" | |||||
android:orientation="horizontal" | android:orientation="horizontal" | ||||
android:paddingTop="8dp" | android:paddingTop="8dp" | ||||
android:paddingBottom="8dp"> | android:paddingBottom="8dp"> | ||||
@@ -51,10 +51,10 @@ | |||||
tools:text="@tools:sample/full_names" /> | tools:text="@tools:sample/full_names" /> | ||||
<CheckBox | <CheckBox | ||||
android:id="@+id/excluded_checkbox" | |||||
android:id="@+id/selected_checkbox" | |||||
android:layout_width="wrap_content" | android:layout_width="wrap_content" | ||||
android:layout_height="wrap_content" | android:layout_height="wrap_content" | ||||
android:checked="@={item.excludedFromTunnel}" | |||||
android:checked="@={item.selected}" | |||||
tools:checked="true" /> | tools:checked="true" /> | ||||
</LinearLayout> | </LinearLayout> | ||||
@@ -220,8 +220,8 @@ | |||||
android:layout_width="0dp" | android:layout_width="0dp" | ||||
android:layout_height="wrap_content" | android:layout_height="wrap_content" | ||||
android:layout_margin="4dp" | 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" | android:textColor="?attr/colorSecondary" | ||||
app:layout_constraintBottom_toBottomOf="parent" | app:layout_constraintBottom_toBottomOf="parent" | ||||
app:layout_constraintEnd_toEndOf="parent" | app:layout_constraintEnd_toEndOf="parent" | ||||
@@ -159,7 +159,7 @@ | |||||
</com.google.android.material.textfield.TextInputLayout> | </com.google.android.material.textfield.TextInputLayout> | ||||
<CheckBox | <CheckBox | ||||
android:id="@+id/excluded_checkbox" | |||||
android:id="@+id/selected_checkbox" | |||||
android:layout_width="wrap_content" | android:layout_width="wrap_content" | ||||
android:layout_height="match_parent" | android:layout_height="match_parent" | ||||
android:layout_marginStart="4dp" | android:layout_marginStart="4dp" | ||||
@@ -76,7 +76,6 @@ | |||||
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string> | <string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string> | ||||
<string name="error_up">Fehler beim Starten 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="exclude_private_ips">Private IPs ausschließen</string> | ||||
<string name="excluded_applications">Ausgeschlossene Anwendungen</string> | |||||
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string> | <string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string> | ||||
<string name="generic_error">Unbekannter „%s“ Fehler</string> | <string name="generic_error">Unbekannter „%s“ Fehler</string> | ||||
<string name="hint_automatic">(auto)</string> | <string name="hint_automatic">(auto)</string> | ||||
@@ -142,7 +141,6 @@ | |||||
<string name="restore_on_boot_title">Beim Neustart wiederherstellen</string> | <string name="restore_on_boot_title">Beim Neustart wiederherstellen</string> | ||||
<string name="save">Speichern</string> | <string name="save">Speichern</string> | ||||
<string name="select_all">Alle auswählen</string> | <string name="select_all">Alle auswählen</string> | ||||
<string name="set_exclusions">Ausnahmen festlegen</string> | |||||
<string name="settings">Einstellungen</string> | <string name="settings">Einstellungen</string> | ||||
<string name="shell_exit_status_read_error">Shell kann den Exit-Status nicht lesen</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_marker_count_error">Die Shell erwartete 4 Marker, erhielt aber %d</string> | ||||
@@ -72,7 +72,6 @@ | |||||
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string> | <string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string> | ||||
<string name="error_up">टनल को लाने में त्रुटि: %s</string> | <string name="error_up">टनल को लाने में त्रुटि: %s</string> | ||||
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</string> | <string name="exclude_private_ips">निजी आईपी को छोड़ दें</string> | ||||
<string name="excluded_applications">निकाले गए ऐप्स</string> | |||||
<string name="generic_error">अज्ञात “%s” त्रुटि</string> | <string name="generic_error">अज्ञात “%s” त्रुटि</string> | ||||
<string name="hint_automatic">(ऑटो)</string> | <string name="hint_automatic">(ऑटो)</string> | ||||
<string name="hint_generated">(उत्पन्न)</string> | <string name="hint_generated">(उत्पन्न)</string> | ||||
@@ -128,7 +127,6 @@ | |||||
<string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string> | <string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string> | ||||
<string name="save">सहेजें</string> | <string name="save">सहेजें</string> | ||||
<string name="select_all">सभी का चयन करे</string> | <string name="select_all">सभी का चयन करे</string> | ||||
<string name="set_exclusions">बहिष्करण सेट करें</string> | |||||
<string name="settings">सेटिंग्स</string> | <string name="settings">सेटिंग्स</string> | ||||
<string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string> | <string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string> | ||||
<string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string> | <string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string> | ||||
@@ -70,7 +70,6 @@ | |||||
<string name="error_root">Izinkan akses root dan coba lagi</string> | <string name="error_root">Izinkan akses root dan coba lagi</string> | ||||
<string name="error_up">Kesalahan menambahkan tunel: %s</string> | <string name="error_up">Kesalahan menambahkan tunel: %s</string> | ||||
<string name="exclude_private_ips">Kecualikan IP pribadi</string> | <string name="exclude_private_ips">Kecualikan IP pribadi</string> | ||||
<string name="excluded_applications">Kecualikan aplikasi</string> | |||||
<string name="generate_new_private_key">Buat kunci privat baru</string> | <string name="generate_new_private_key">Buat kunci privat baru</string> | ||||
<string name="generic_error">Eror “%s” Tidak diketahui</string> | <string name="generic_error">Eror “%s” Tidak diketahui</string> | ||||
<string name="hint_automatic">(otomatis)</string> | <string name="hint_automatic">(otomatis)</string> | ||||
@@ -136,7 +135,6 @@ | |||||
<string name="restore_on_boot_title">Pulihkan saat boot</string> | <string name="restore_on_boot_title">Pulihkan saat boot</string> | ||||
<string name="save">Simpan</string> | <string name="save">Simpan</string> | ||||
<string name="select_all">Pilih semua</string> | <string name="select_all">Pilih semua</string> | ||||
<string name="set_exclusions">Tetapkan pengecualian</string> | |||||
<string name="settings">Pengaturan</string> | <string name="settings">Pengaturan</string> | ||||
<string name="shell_exit_status_read_error">Shell tidak dapat membaca status keluar</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_marker_count_error">Shell diharapkan 4 nilai, diterima %d</string> | ||||
@@ -76,7 +76,6 @@ | |||||
<string name="error_root">Accedi come root e riprova</string> | <string name="error_root">Accedi come root e riprova</string> | ||||
<string name="error_up">Errore di attivazione 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="exclude_private_ips">Escludi IP privati</string> | ||||
<string name="excluded_applications">Applicazioni escluse</string> | |||||
<string name="generate_new_private_key">Genera nuova chiave privata</string> | <string name="generate_new_private_key">Genera nuova chiave privata</string> | ||||
<string name="generic_error">Errore “%s” sconosciuto</string> | <string name="generic_error">Errore “%s” sconosciuto</string> | ||||
<string name="hint_automatic">(auto)</string> | <string name="hint_automatic">(auto)</string> | ||||
@@ -142,7 +141,6 @@ | |||||
<string name="restore_on_boot_title">Ripristina all\'avvio</string> | <string name="restore_on_boot_title">Ripristina all\'avvio</string> | ||||
<string name="save">Salva</string> | <string name="save">Salva</string> | ||||
<string name="select_all">Seleziona tutto</string> | <string name="select_all">Seleziona tutto</string> | ||||
<string name="set_exclusions">Imposta esclusioni</string> | |||||
<string name="settings">Impostazioni</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_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_marker_count_error">La shell si aspettava 4 marker, ne ha ricevuti %d</string> | ||||
@@ -70,7 +70,6 @@ | |||||
<string name="error_root">root 権限を取得して再試行してください</string> | <string name="error_root">root 権限を取得して再試行してください</string> | ||||
<string name="error_up">トンネル起動時エラー: %s</string> | <string name="error_up">トンネル起動時エラー: %s</string> | ||||
<string name="exclude_private_ips">プライベート IP アドレスを除外</string> | <string name="exclude_private_ips">プライベート IP アドレスを除外</string> | ||||
<string name="excluded_applications">対象外とするアプリケーション</string> | |||||
<string name="generate_new_private_key">新しい秘密鍵を生成する</string> | <string name="generate_new_private_key">新しい秘密鍵を生成する</string> | ||||
<string name="generic_error">未知の “%s” エラー</string> | <string name="generic_error">未知の “%s” エラー</string> | ||||
<string name="hint_automatic">(自動)</string> | <string name="hint_automatic">(自動)</string> | ||||
@@ -136,7 +135,6 @@ | |||||
<string name="restore_on_boot_title">起動時に復元</string> | <string name="restore_on_boot_title">起動時に復元</string> | ||||
<string name="save">保存</string> | <string name="save">保存</string> | ||||
<string name="select_all">すべて選択</string> | <string name="select_all">すべて選択</string> | ||||
<string name="set_exclusions">対象外アプリを設定</string> | |||||
<string name="settings">設定</string> | <string name="settings">設定</string> | ||||
<string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string> | <string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string> | ||||
<string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました</string> | <string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました</string> | ||||
@@ -88,7 +88,6 @@ | |||||
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string> | <string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string> | ||||
<string name="error_up">Ошибка при запуске туннеля: %s</string> | <string name="error_up">Ошибка при запуске туннеля: %s</string> | ||||
<string name="exclude_private_ips">Исключить частные IP-адреса</string> | <string name="exclude_private_ips">Исключить частные IP-адреса</string> | ||||
<string name="excluded_applications">Исключенные приложения</string> | |||||
<string name="generate_new_private_key">Сгенерировать новый приватный ключ</string> | <string name="generate_new_private_key">Сгенерировать новый приватный ключ</string> | ||||
<string name="generic_error">Неизвестная “%s” ошибка</string> | <string name="generic_error">Неизвестная “%s” ошибка</string> | ||||
<string name="hint_automatic">(авто)</string> | <string name="hint_automatic">(авто)</string> | ||||
@@ -154,7 +153,6 @@ | |||||
<string name="restore_on_boot_title">Восстанавливать при загрузке</string> | <string name="restore_on_boot_title">Восстанавливать при загрузке</string> | ||||
<string name="save">Сохранить</string> | <string name="save">Сохранить</string> | ||||
<string name="select_all">Выбрать все</string> | <string name="select_all">Выбрать все</string> | ||||
<string name="set_exclusions">ОК</string> | |||||
<string name="settings">Настройки</string> | <string name="settings">Настройки</string> | ||||
<string name="shell_exit_status_read_error">Shell не может прочитать статус выхода</string> | <string name="shell_exit_status_read_error">Shell не может прочитать статус выхода</string> | ||||
<string name="shell_marker_count_error">Shell ожидает 4 маркера, получено %d</string> | <string name="shell_marker_count_error">Shell ожидает 4 маркера, получено %d</string> | ||||
@@ -65,7 +65,7 @@ | |||||
<string name="config_file_exists_error">Konfiguracijska datoteka 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_not_found_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče najti</string> | ||||
<string name="config_rename_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče preimenovati</string> | <string name="config_rename_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče preimenovati</string> | ||||
<string name="config_save_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče shraniti: %2$s</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="config_save_success">Konfiguracijska datoteka za „%s“ uspešno shranjena</string> | ||||
<string name="create_activity_title">Ustvarite WireGuard tunel</string> | <string name="create_activity_title">Ustvarite WireGuard tunel</string> | ||||
<string name="create_bin_dir_error">Lokalnega imenika za aplikacijo ni bilo mogoče kreirati</string> | <string name="create_bin_dir_error">Lokalnega imenika za aplikacijo ni bilo mogoče kreirati</string> | ||||
@@ -88,7 +88,6 @@ | |||||
<string name="error_root">Prosim omogočite root dostop in poskusite ponovno</string> | <string name="error_root">Prosim omogočite root dostop in poskusite ponovno</string> | ||||
<string name="error_up">Napaka pri vzpostavitvi tunela: %s</string> | <string name="error_up">Napaka pri vzpostavitvi tunela: %s</string> | ||||
<string name="exclude_private_ips">Izključitev privatnih IP naslovov</string> | <string name="exclude_private_ips">Izključitev privatnih IP naslovov</string> | ||||
<string name="excluded_applications">Izključitev aplikacije</string> | |||||
<string name="generate_new_private_key">Generiraj nov privatni ključ</string> | <string name="generate_new_private_key">Generiraj nov privatni ključ</string> | ||||
<string name="generic_error">Neznana napaka „%s“</string> | <string name="generic_error">Neznana napaka „%s“</string> | ||||
<string name="hint_automatic">(samodejno)</string> | <string name="hint_automatic">(samodejno)</string> | ||||
@@ -154,7 +153,6 @@ | |||||
<string name="restore_on_boot_title">Ponovna vzpostavitev pri ponovnem zagonu</string> | <string name="restore_on_boot_title">Ponovna vzpostavitev pri ponovnem zagonu</string> | ||||
<string name="save">Shrani</string> | <string name="save">Shrani</string> | ||||
<string name="select_all">Izbor vseh</string> | <string name="select_all">Izbor vseh</string> | ||||
<string name="set_exclusions">Določite izključitve</string> | |||||
<string name="settings">Nastavitve</string> | <string name="settings">Nastavitve</string> | ||||
<string name="shell_exit_status_read_error">Lupina ne more prebrati izhodnega statusa</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 markerje, dobila pa je %d</string> | <string name="shell_marker_count_error">Lupina je pričakovala 4 markerje, dobila pa je %d</string> | ||||
@@ -70,7 +70,6 @@ | |||||
<string name="error_root">请获取 root 权限并重试</string> | <string name="error_root">请获取 root 权限并重试</string> | ||||
<string name="error_up">建立连接时出错:%s</string> | <string name="error_up">建立连接时出错:%s</string> | ||||
<string name="exclude_private_ips">排除局域网</string> | <string name="exclude_private_ips">排除局域网</string> | ||||
<string name="excluded_applications">排除的应用</string> | |||||
<string name="generate_new_private_key">生成新的私钥</string> | <string name="generate_new_private_key">生成新的私钥</string> | ||||
<string name="generic_error">未知的 “%s” 错误</string> | <string name="generic_error">未知的 “%s” 错误</string> | ||||
<string name="hint_automatic">(自动)</string> | <string name="hint_automatic">(自动)</string> | ||||
@@ -136,7 +135,6 @@ | |||||
<string name="restore_on_boot_title">启动时恢复</string> | <string name="restore_on_boot_title">启动时恢复</string> | ||||
<string name="save">保存</string> | <string name="save">保存</string> | ||||
<string name="select_all">全选</string> | <string name="select_all">全选</string> | ||||
<string name="set_exclusions">确定</string> | |||||
<string name="settings">设置</string> | <string name="settings">设置</string> | ||||
<string name="shell_exit_status_read_error">Shell 无法读取退出状态</string> | <string name="shell_exit_status_read_error">Shell 无法读取退出状态</string> | ||||
<string name="shell_marker_count_error">Shell 应获取 4 个标记,获取到 %d 个</string> | <string name="shell_marker_count_error">Shell 应获取 4 个标记,获取到 %d 个</string> | ||||
@@ -24,6 +24,22 @@ | |||||
<item quantity="one">%d Excluded Application</item> | <item quantity="one">%d Excluded Application</item> | ||||
<item quantity="other">%d Excluded Applications</item> | <item quantity="other">%d Excluded Applications</item> | ||||
</plurals> | </plurals> | ||||
<plurals name="set_included_applications"> | |||||
<item quantity="one">%d Included Application</item> | |||||
<item quantity="other">%d Included Applications</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 apps</item> | |||||
<item quantity="other">Include %d apps</item> | |||||
</plurals> | |||||
<plurals name="exclude_n_applications"> | |||||
<item quantity="one">Exclude %d apps</item> | |||||
<item quantity="other">Exclude %d apps</item> | |||||
</plurals> | |||||
<string name="use_all_applications">Use all apps</string> | |||||
<string name="add_peer">Add peer</string> | <string name="add_peer">Add peer</string> | ||||
<string name="addresses">Addresses</string> | <string name="addresses">Addresses</string> | ||||
<string name="allow_remote_control_intents_summary_off">External apps may not toggle tunnels (recommended)</string> | <string name="allow_remote_control_intents_summary_off">External apps may not toggle tunnels (recommended)</string> | ||||
@@ -76,7 +92,6 @@ | |||||
<string name="error_root">Please obtain root access and try again</string> | <string name="error_root">Please obtain root access and try again</string> | ||||
<string name="error_up">Error bringing up tunnel: %s</string> | <string name="error_up">Error bringing up tunnel: %s</string> | ||||
<string name="exclude_private_ips">Exclude private IPs</string> | <string name="exclude_private_ips">Exclude private IPs</string> | ||||
<string name="excluded_applications">Excluded Applications</string> | |||||
<string name="generate_new_private_key">Generate new private key</string> | <string name="generate_new_private_key">Generate new private key</string> | ||||
<string name="generic_error">Unknown “%s” error</string> | <string name="generic_error">Unknown “%s” error</string> | ||||
<string name="hint_automatic">(auto)</string> | <string name="hint_automatic">(auto)</string> | ||||
@@ -142,7 +157,6 @@ | |||||
<string name="restore_on_boot_title">Restore on boot</string> | <string name="restore_on_boot_title">Restore on boot</string> | ||||
<string name="save">Save</string> | <string name="save">Save</string> | ||||
<string name="select_all">Select all</string> | <string name="select_all">Select all</string> | ||||
<string name="set_exclusions">Set Exclusions</string> | |||||
<string name="settings">Settings</string> | <string name="settings">Settings</string> | ||||
<string name="shell_exit_status_read_error">Shell cannot read exit status</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_marker_count_error">Shell expected 4 markers, received %d</string> | ||||