diff options
Diffstat (limited to 'ui')
7 files changed, 152 insertions, 40 deletions
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt index 2aec921a..bb3e76a7 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt @@ -5,6 +5,7 @@ package com.wireguard.android.fragment import android.os.Bundle +import android.util.Log; import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -21,6 +22,7 @@ import com.wireguard.android.databinding.TunnelDetailFragmentBinding import com.wireguard.android.databinding.TunnelDetailPeerBinding import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.util.QuantityFormatter +import com.wireguard.android.viewmodel.ConfigDetail import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.time.Duration @@ -80,7 +82,9 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } else { lifecycleScope.launch { try { - binding.config = newTunnel.getConfigAsync() + var config = newTunnel.getConfigDetailAsync() + binding.config = config + Log.i(TAG, "onSelectedTunnelChanged " + config + ", " + config.config) } catch (_: Throwable) { binding.config = null } @@ -115,7 +119,7 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { for (i in 0 until binding.peersLayout.childCount) { val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i)) ?: continue - val publicKey = peer.item!!.publicKey + val publicKey = peer.item!!.peer.publicKey val rx = statistics.peerRx(publicKey) val tx = statistics.peerTx(publicKey) if (rx == 0L && tx == 0L) { @@ -147,4 +151,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider { } } } + + companion object { + private const val TAG = "WireGuard/TunnelDetailFragment" + } } diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt index 49e080f9..d4a0b1c0 100644 --- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt +++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt @@ -13,7 +13,11 @@ import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel import com.wireguard.android.databinding.Keyed import com.wireguard.android.util.applicationScope +import com.wireguard.android.viewmodel.ConfigDetail import com.wireguard.config.Config +import com.wireguard.config.InetEndpoint +import com.wireguard.crypto.Key; +import java.util.Optional; import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -59,6 +63,12 @@ class ObservableTunnel internal constructor( if (state != Tunnel.State.UP) { onStatisticsChanged(null) onDhcpChanged(null) + onEndpointChange(null, null) + } else { + configDetail?.peers?.forEach { + var endpoint: InetEndpoint? = it.peer.endpoint.orElse(null) + it.endpoint = Optional.ofNullable(endpoint?.getResolved()?.get()) + } } this.state = state notifyPropertyChanged(BR.state) @@ -72,6 +82,7 @@ class ObservableTunnel internal constructor( this@ObservableTunnel.state } + private var configDetail: ConfigDetail? = if (config != null) ConfigDetail(config) else null @get:Bindable var config = config @@ -90,7 +101,11 @@ class ObservableTunnel internal constructor( private set suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) { - config ?: manager.getTunnelConfig(this@ObservableTunnel) + config ?: manager.getTunnelConfig(this@ObservableTunnel).config!! + } + + suspend fun getConfigDetailAsync(): ConfigDetail = withContext(Dispatchers.Main.immediate) { + configDetail ?: manager.getTunnelConfig(this@ObservableTunnel) } suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) { @@ -102,10 +117,11 @@ class ObservableTunnel internal constructor( } } - fun onConfigChanged(config: Config?): Config? { + fun onConfigChanged(config: Config?): ConfigDetail? { this.config = config + this.configDetail = ConfigDetail(config) notifyPropertyChanged(BR.config) - return config + return configDetail } @@ -155,6 +171,20 @@ class ObservableTunnel internal constructor( return dhcp } + override fun onEndpointChange(publicKey: Key?, newEndpoint: InetEndpoint?) { + Log.i(TAG, "ObservableTunnel onEndpointChange " + newEndpoint) + configDetail?.peers?.forEach { + Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer) + if (publicKey == null || it.peer.publicKey.equals(publicKey)) { + if (newEndpoint == null) { + it.endpoint = it.peer.endpoint + } else { + it.endpoint = newEndpoint.getResolved() + } + } + } + } + suspend fun deleteAsync() = manager.delete(this) diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index e7bb751b..77d69f70 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -24,6 +24,7 @@ import com.wireguard.android.databinding.ObservableSortedKeyedArrayList import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.UserKnobs import com.wireguard.android.util.applicationScope +import com.wireguard.android.viewmodel.ConfigDetail import com.wireguard.config.Config import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers @@ -94,7 +95,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) } } - suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) { + suspend fun getTunnelConfig(tunnel: ObservableTunnel): ConfigDetail = withContext(Dispatchers.Main.immediate) { tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!! } @@ -155,7 +156,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { tunnel.onConfigChanged(withContext(Dispatchers.IO) { getBackend().setState(tunnel, tunnel.state, config) configStore.save(tunnel.name, config) - })!! + })!!.config!! } suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) { diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt new file mode 100644 index 00000000..af95a86a --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt @@ -0,0 +1,22 @@ +package com.wireguard.android.viewmodel + +import androidx.databinding.ObservableArrayList +import androidx.databinding.ObservableList + +import com.wireguard.config.Config + +class ConfigDetail { + val config: Config? + val peers: ObservableList<PeerDetail> = ObservableArrayList() + + constructor(other: Config?) { + config = other + if (other != null) { + other.peers.forEach { + val detail = PeerDetail(it) + peers.add(detail) + detail.bind(this) + } + } + } +} diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt new file mode 100644 index 00000000..abc81998 --- /dev/null +++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt @@ -0,0 +1,51 @@ +package com.wireguard.android.viewmodel + +import android.util.Log +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import androidx.databinding.Observable + +import com.wireguard.android.BR +import com.wireguard.config.InetEndpoint +import com.wireguard.config.Peer + +import java.util.Optional; + + +class PeerDetail : BaseObservable { + var peer: Peer + private var owner: ConfigDetail? = null + + @get:Bindable + var endpoint: Optional<InetEndpoint> = Optional.empty() + get() { + if (!field.isEmpty()) { + return field + } else { + return peer.endpoint + } + } + + set(value) { + Log.i(TAG, "notifyPropertyChanged endpoint " + this + ", " + value) + field = value + notifyPropertyChanged(BR.endpoint) + } + + constructor(other: Peer) { + peer = other + } + + fun bind(owner: ConfigDetail) { + this.owner = owner + } + + override fun addOnPropertyChangedCallback (callback: Observable.OnPropertyChangedCallback) { + Log.i(TAG, "addOnPropertyChangedCallback " + this + ", " + callback) + super.addOnPropertyChangedCallback(callback) + } + + companion object { + private const val TAG = "WireGuard/PeerDetail" + } +} diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml index d869216e..1c4c25d6 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -21,7 +21,7 @@ <variable name="config" - type="com.wireguard.config.Config" /> + type="com.wireguard.android.viewmodel.ConfigDetail" /> </data> <ScrollView @@ -119,7 +119,7 @@ android:nextFocusForward="@id/addresses_text" android:onClick="@{ClipboardUtils::copyTextView}" android:singleLine="true" - android:text="@{config.interface.keyPair.publicKey.toBase64}" + android:text="@{config.config.interface.keyPair.publicKey.toBase64}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/public_key_label" tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" /> @@ -131,7 +131,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/addresses_text" android:text="@string/addresses" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -145,8 +145,8 @@ android:nextFocusDown="@id/dhcp_addresses_text" android:nextFocusForward="@id/dhcp_addresses_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.addresses}" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.addresses}" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/addresses_label" tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> @@ -185,7 +185,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_servers_text" android:text="@string/dns_servers" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dhcp_addresses_text" /> @@ -199,8 +199,8 @@ android:nextFocusDown="@id/dns_search_domains_text" android:nextFocusForward="@id/dns_search_domains_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsServers}" - android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.dnsServers}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_servers_label" tools:text="8.8.8.8, 8.8.4.4" /> @@ -212,7 +212,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_search_domain_text" android:text="@string/dns_search_domains" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> @@ -226,8 +226,8 @@ android:nextFocusDown="@id/listen_port_text" android:nextFocusForward="@id/listen_port_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsSearchDomains}" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.dnsSearchDomains}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label" tools:text="zx2c4.com" /> @@ -239,7 +239,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/listen_port_text" android:text="@string/listen_port" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -256,8 +256,8 @@ android:nextFocusDown="@id/http_proxy_text" android:nextFocusForward="@id/mtu_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.listenPort}" - android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.listenPort}" + android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toStartOf="@id/mtu_label" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toStartOf="parent" @@ -271,7 +271,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/mtu_text" android:text="@string/mtu" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintLeft_toRightOf="@id/listen_port_label" @@ -288,8 +288,8 @@ android:nextFocusUp="@id/dns_servers_text" android:nextFocusForward="@id/http_proxy_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.mtu}" - android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.mtu}" + android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_weight="0.5" app:layout_constraintStart_toEndOf="@id/listen_port_label" @@ -311,7 +311,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/http_proxy_text" android:text="@string/http_proxy" - android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" /> @@ -325,8 +325,8 @@ android:nextFocusDown="@id/applications_text" android:nextFocusForward="@id/applications_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.httpProxy}" - android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.httpProxy}" + android:visibility="@{(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/http_proxy_label" app:layout_constraintStart_toStartOf="parent" tools:text="http://example.com:8888" /> @@ -338,7 +338,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/applications_text" android:text="@string/applications" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" app:layout_constraintStart_toStartOf="parent" /> @@ -352,8 +352,8 @@ 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:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.config.interface.excludedApplications.size(), config.config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.config.interface.includedApplications.size(), config.config.interface.includedApplications.size())}" + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintTop_toBottomOf="@+id/applications_label" app:layout_constraintStart_toStartOf="parent" tools:text="8 excluded" /> diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml index f295bb13..e70c3df5 100644 --- a/ui/src/main/res/layout/tunnel_detail_peer.xml +++ b/ui/src/main/res/layout/tunnel_detail_peer.xml @@ -9,7 +9,7 @@ <variable name="item" - type="com.wireguard.config.Peer" /> + type="com.wireguard.android.viewmodel.PeerDetail" /> </data> <com.google.android.material.card.MaterialCardView @@ -52,7 +52,7 @@ android:nextFocusForward="@id/pre_shared_key_text" android:onClick="@{ClipboardUtils::copyTextView}" android:singleLine="true" - android:text="@{item.publicKey.toBase64}" + android:text="@{item.peer.publicKey.toBase64}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/public_key_label" tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" /> @@ -64,7 +64,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/pre_shared_key_text" android:text="@string/pre_shared_key" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -81,7 +81,7 @@ android:nextFocusForward="@id/allowed_ips_text" android:singleLine="true" android:text="@string/pre_shared_key_enabled" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label" tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" /> @@ -93,7 +93,7 @@ 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}" + android:visibility="@{item.peer.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_text" /> @@ -107,8 +107,8 @@ android:nextFocusDown="@id/endpoint_text" android:nextFocusForward="@id/endpoint_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{item.allowedIps}" - android:visibility="@{item.allowedIps.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{item.peer.allowedIps}" + android:visibility="@{item.peer.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" /> @@ -147,7 +147,7 @@ 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}" + android:visibility="@{!item.peer.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/endpoint_text" /> @@ -161,8 +161,8 @@ 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:visibility="@{!item.persistentKeepalive.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{@plurals/persistent_keepalive_seconds_unit(item.peer.persistentKeepalive.orElse(0), item.peer.persistentKeepalive.orElse(0))}" + android:visibility="@{!item.peer.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" /> |