diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-12-10 10:30:09 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-12-10 10:30:09 -0300 |
| commit | 36b126a1b8c92445508e901ddcdca6d8ebf940c4 (patch) | |
| tree | 0803e5b8c118969acc239c8adda6a9119f881f2b /android | |
| parent | 4aa77f22a715758c77888f6817d11068c071d9c1 (diff) | |
| parent | ec89a7de3c3aec6260f27c5ce12a768ebee965be (diff) | |
| download | mullvadvpn-36b126a1b8c92445508e901ddcdca6d8ebf940c4.tar.xz mullvadvpn-36b126a1b8c92445508e901ddcdca6d8ebf940c4.zip | |
Merge branch 'android-custom-dns-ui'
Diffstat (limited to 'android')
19 files changed, 690 insertions, 40 deletions
diff --git a/android/build.gradle b/android/build.gradle index 9b45866a31..9a7a8e0d0b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:design:28.0.0' implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'commons-validator:commons-validator:1.7' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.4.10' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' implementation 'joda-time:joda-time:2.10.2' diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/CustomDns.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/CustomDns.kt index 95a4be3cb9..9b41877f4c 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/CustomDns.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/CustomDns.kt @@ -43,13 +43,37 @@ class CustomDns(val daemon: MullvadDaemon, val settingsListener: SettingsListene } } - fun addDnsServer(server: InetAddress) { + fun addDnsServer(server: InetAddress): Boolean { synchronized(this) { if (!dnsServers.contains(server)) { dnsServers.add(server) changeDnsOptions(enabled, dnsServers) + + return true + } + } + + return false + } + + fun replaceDnsServer(oldServer: InetAddress, newServer: InetAddress): Boolean { + synchronized(this) { + if (oldServer == newServer) { + return true + } else if (!dnsServers.contains(newServer)) { + val index = dnsServers.indexOf(oldServer) + + if (index >= 0) { + dnsServers.removeAt(index) + dnsServers.add(index, newServer) + changeDnsOptions(enabled, dnsServers) + + return true + } } } + + return false } fun removeDnsServer(server: InetAddress) { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt index 2e31639676..a63de128fa 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt @@ -1,15 +1,23 @@ package net.mullvad.mullvadvpn.ui import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.Settings +import net.mullvad.mullvadvpn.ui.customdns.CustomDnsAdapter +import net.mullvad.mullvadvpn.ui.widget.CellSwitch +import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView import net.mullvad.mullvadvpn.ui.widget.MtuCell import net.mullvad.mullvadvpn.ui.widget.NavigateCell +import net.mullvad.mullvadvpn.ui.widget.ToggleCell +import net.mullvad.mullvadvpn.util.AdapterWithHeader class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) { + private lateinit var customDnsAdapter: CustomDnsAdapter + private lateinit var customDnsToggle: ToggleCell private lateinit var wireguardMtuInput: MtuCell private lateinit var titleController: CollapsibleTitleController @@ -21,9 +29,44 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) { val view = inflater.inflate(R.layout.advanced, container, false) view.findViewById<View>(R.id.back).setOnClickListener { + customDnsAdapter.stopEditing() parentActivity.onBackPressed() } + titleController = CollapsibleTitleController(view, R.id.contents) + + customDnsAdapter = CustomDnsAdapter(customDns) + + view.findViewById<CustomRecyclerView>(R.id.contents).apply { + layoutManager = LinearLayoutManager(parentActivity) + + adapter = AdapterWithHeader(customDnsAdapter, R.layout.advanced_header).apply { + onHeaderAvailable = { headerView -> + configureHeader(headerView) + titleController.expandedTitleView = headerView.findViewById(R.id.expanded_title) + } + } + + addItemDecoration( + ListItemDividerDecoration(parentActivity).apply { + topOffsetId = R.dimen.list_item_divider + } + ) + } + + attachBackButtonHandler() + + return view + } + + override fun onSafelyDestroyView() { + detachBackButtonHandler() + customDnsAdapter.onDestroy() + titleController.onDestroy() + settingsListener.unsubscribe(this) + } + + private fun configureHeader(view: View) { wireguardMtuInput = view.findViewById<MtuCell>(R.id.wireguard_mtu).apply { onSubmit = { mtu -> jobTracker.newBackgroundJob("updateMtu") { @@ -40,13 +83,31 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) { targetFragment = SplitTunnelingFragment::class } - settingsListener.subscribe(this) { settings -> - updateUi(settings) + customDnsToggle = view.findViewById<ToggleCell>(R.id.enable_custom_dns).apply { + listener = { state -> + jobTracker.newBackgroundJob("toggleCustomDns") { + if (state == CellSwitch.State.ON) { + customDns.enable() + } else { + customDns.disable() + } + } + } } - titleController = CollapsibleTitleController(view) + customDns.onEnabledChanged.subscribe(this) { enabled -> + jobTracker.newUiJob("updateEnabled") { + if (enabled) { + customDnsToggle.state = CellSwitch.State.ON + } else { + customDnsToggle.state = CellSwitch.State.OFF + } + } + } - return view + settingsListener.subscribe(this) { settings -> + updateUi(settings) + } } private fun updateUi(settings: Settings) { @@ -57,8 +118,18 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) { } } - override fun onSafelyDestroyView() { - titleController.onDestroy() - settingsListener.unsubscribe(this) + private fun attachBackButtonHandler() { + parentActivity.backButtonHandler = { + if (customDnsAdapter.isEditing) { + customDnsAdapter.stopEditing() + true + } else { + false + } + } + } + + private fun detachBackButtonHandler() { + parentActivity.backButtonHandler = null } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt new file mode 100644 index 0000000000..1d0f940d4b --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt @@ -0,0 +1,16 @@ +package net.mullvad.mullvadvpn.ui.customdns + +import android.view.View +import net.mullvad.mullvadvpn.R + +class AddCustomDnsServerHolder(view: View, adapter: CustomDnsAdapter) : CustomDnsItemHolder(view) { + init { + view.findViewById<View>(R.id.add).setOnClickListener { + adapter.newDnsServer() + } + + view.findViewById<View>(R.id.click_area).setOnClickListener { + adapter.newDnsServer() + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt new file mode 100644 index 0000000000..5c3eeee3a5 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt @@ -0,0 +1,291 @@ +package net.mullvad.mullvadvpn.ui.customdns + +import android.support.v7.widget.RecyclerView.Adapter +import android.view.LayoutInflater +import android.view.ViewGroup +import java.net.InetAddress +import kotlin.properties.Delegates.observable +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.service.CustomDns +import net.mullvad.mullvadvpn.util.JobTracker +import org.apache.commons.validator.routines.InetAddressValidator + +class CustomDnsAdapter(val customDns: CustomDns) : Adapter<CustomDnsItemHolder>() { + private enum class ViewTypes { + ADD_SERVER, + EDIT_SERVER, + SHOW_SERVER, + FOOTER, + } + + private val customDnsServersLock = Mutex() + private val inetAddressValidator = InetAddressValidator.getInstance() + private val jobTracker = JobTracker() + + private var editingPosition: Int? = null + + private var activeCustomDnsServers + by observable<List<InetAddress>>(emptyList()) { _, _, servers -> + if (servers != cachedCustomDnsServers) { + cachedCustomDnsServers = servers.toMutableList() + notifyDataSetChanged() + } + } + + private var cachedCustomDnsServers = emptyList<InetAddress>().toMutableList() + + private var enabled by observable(false) { _, oldValue, newValue -> + if (oldValue != newValue) { + if (newValue == true) { + notifyItemRangeInserted(0, cachedCustomDnsServers.size + 1) + + if (cachedCustomDnsServers.isEmpty()) { + editingPosition = 0 + } + } else { + notifyItemRangeRemoved(0, cachedCustomDnsServers.size + 1) + } + } + } + + val isEditing + get() = editingPosition != null + + init { + customDns.apply { + onDnsServersChanged.subscribe(this) { dnsServers -> + jobTracker.newUiJob("updateDnsServers") { + customDnsServersLock.withLock { + activeCustomDnsServers = dnsServers + } + } + } + + onEnabledChanged.subscribe(this) { value -> + jobTracker.newUiJob("updateEnabled") { + customDnsServersLock.withLock { + enabled = value + } + } + } + } + } + + override fun getItemCount() = + if (enabled) { + cachedCustomDnsServers.size + 2 + } else { + 1 + } + + override fun getItemViewType(position: Int): Int { + val count = getItemCount() + val footer = count - 1 + val addServer = count - 2 + + if (position == footer) { + return ViewTypes.FOOTER.ordinal + } else if (position == editingPosition) { + return ViewTypes.EDIT_SERVER.ordinal + } else if (position == addServer) { + return ViewTypes.ADD_SERVER.ordinal + } else { + return ViewTypes.SHOW_SERVER.ordinal + } + } + + override fun onCreateViewHolder(parentView: ViewGroup, type: Int): CustomDnsItemHolder { + val inflater = LayoutInflater.from(parentView.context) + + when (ViewTypes.values()[type]) { + ViewTypes.FOOTER -> { + val view = inflater.inflate(R.layout.custom_dns_footer, parentView, false) + return CustomDnsFooterHolder(view) + } + ViewTypes.ADD_SERVER -> { + val view = inflater.inflate(R.layout.add_custom_dns_server, parentView, false) + return AddCustomDnsServerHolder(view, this) + } + ViewTypes.EDIT_SERVER -> { + val view = inflater.inflate(R.layout.edit_custom_dns_server, parentView, false) + return EditCustomDnsServerHolder(view, this) + } + ViewTypes.SHOW_SERVER -> { + val view = inflater.inflate(R.layout.custom_dns_server, parentView, false) + return CustomDnsServerHolder(view, this) + } + } + } + + override fun onBindViewHolder(holder: CustomDnsItemHolder, position: Int) { + if (holder is CustomDnsServerHolder) { + holder.serverAddress = cachedCustomDnsServers[position] + } else if (holder is EditCustomDnsServerHolder) { + if (position >= cachedCustomDnsServers.size) { + holder.serverAddress = null + } else { + holder.serverAddress = cachedCustomDnsServers[position] + } + } + } + + fun onDestroy() { + customDns.apply { + onDnsServersChanged.unsubscribe(this) + onEnabledChanged.unsubscribe(this) + } + } + + fun newDnsServer() { + jobTracker.newUiJob("newDnsServer") { + customDnsServersLock.withLock { + if (enabled) { + val count = getItemCount() + + editDnsServerAt(count - 2) + } + } + } + } + + fun saveDnsServer(address: String, errorCallback: () -> Unit) { + jobTracker.newUiJob("saveDnsServer $address") { + customDnsServersLock.withLock { + editingPosition?.let { position -> + var validAddress: Boolean + + if (position >= cachedCustomDnsServers.size) { + validAddress = addDnsServer(address) + } else { + validAddress = replaceDnsServer(address, position) + } + + if (!validAddress) { + errorCallback() + } + } + } + } + } + + fun editDnsServer(address: InetAddress) { + jobTracker.newUiJob("editDnsServer $address") { + customDnsServersLock.withLock { + if (enabled) { + val position = cachedCustomDnsServers.indexOf(address) + + editDnsServerAt(position) + } + } + } + } + + fun stopEditing() { + jobTracker.newUiJob("stopEditing") { + customDnsServersLock.withLock { + if (enabled) { + editDnsServerAt(null) + } + } + } + } + + fun stopEditing(address: InetAddress) { + jobTracker.newUiJob("stopEditing $address") { + customDnsServersLock.withLock { + if (enabled) { + editingPosition?.let { position -> + if (cachedCustomDnsServers.getOrNull(position) == address) { + editDnsServerAt(null) + } + } + } + } + } + } + + fun removeDnsServer(address: InetAddress) { + jobTracker.newUiJob("removeDnsServer $address") { + customDnsServersLock.withLock { + val position = jobTracker.runOnBackground { + val index = cachedCustomDnsServers.indexOf(address) + + cachedCustomDnsServers.removeAt(index) + customDns.removeDnsServer(address) + + index + } + + notifyItemRemoved(position) + } + } + } + + private suspend fun addDnsServer(addressText: String): Boolean { + var added = false + + withValidAddress(addressText) { address -> + if (customDns.addDnsServer(address)) { + cachedCustomDnsServers.add(address) + added = true + } + } + + if (added) { + editingPosition = null + + val count = getItemCount() + + notifyItemChanged(count - 3) + notifyItemInserted(count - 2) + } + + return added + } + + private suspend fun replaceDnsServer(address: String, position: Int): Boolean { + var replaced = false + + withValidAddress(address) { newAddress -> + val oldAddress = cachedCustomDnsServers[position] + + if (customDns.replaceDnsServer(oldAddress, newAddress)) { + cachedCustomDnsServers[position] = newAddress + replaced = true + } + } + + if (replaced) { + editingPosition = null + notifyItemChanged(position) + } + + return replaced + } + + private fun editDnsServerAt(position: Int?) { + editingPosition?.let { oldPosition -> + notifyItemChanged(oldPosition) + } + + editingPosition = position + + position?.let { newPosition -> + notifyItemChanged(newPosition) + } + } + + private suspend fun withValidAddress(addressText: String, handler: (InetAddress) -> Unit) { + jobTracker.runOnBackground { + if (inetAddressValidator.isValid(addressText)) { + val address = InetAddress.getByName(addressText) + + if (!address.isLoopbackAddress()) { + handler(address) + } + } + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt new file mode 100644 index 0000000000..d09beffbce --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.ui.customdns + +import android.view.View + +class CustomDnsFooterHolder(view: View) : CustomDnsItemHolder(view) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt new file mode 100644 index 0000000000..3276737e5d --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.ui.customdns + +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.View + +abstract class CustomDnsItemHolder(view: View) : ViewHolder(view) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt new file mode 100644 index 0000000000..49efad9310 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt @@ -0,0 +1,30 @@ +package net.mullvad.mullvadvpn.ui.customdns + +import android.view.View +import android.widget.TextView +import java.net.InetAddress +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.talpid.util.addressString + +class CustomDnsServerHolder(view: View, adapter: CustomDnsAdapter) : CustomDnsItemHolder(view) { + private val label: TextView = view.findViewById(R.id.label) + + var serverAddress by observable<InetAddress?>(null) { _, _, address -> + label.text = address?.addressString() ?: "" + } + + init { + view.findViewById<View>(R.id.click_area).setOnClickListener { + serverAddress?.let { address -> + adapter.editDnsServer(address) + } + } + + view.findViewById<View>(R.id.remove).setOnClickListener { + serverAddress?.let { address -> + adapter.removeDnsServer(address) + } + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt new file mode 100644 index 0000000000..560bfb22d8 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt @@ -0,0 +1,62 @@ +package net.mullvad.mullvadvpn.ui.customdns + +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.View.OnFocusChangeListener +import android.widget.EditText +import java.net.InetAddress +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.talpid.util.addressString + +class EditCustomDnsServerHolder(view: View, adapter: CustomDnsAdapter) : CustomDnsItemHolder(view) { + private val context = view.context + private val errorColor = context.getColor(R.color.red) + private val normalColor = context.getColor(R.color.blue) + + private val input: EditText = view.findViewById<EditText>(R.id.input).apply { + onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + serverAddress?.let { address -> + adapter.stopEditing(address) + } + } + } + } + + private val watcher = object : TextWatcher { + override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {} + + override fun afterTextChanged(text: Editable) { + input.setTextColor(normalColor) + input.removeTextChangedListener(this) + } + + override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {} + } + + var serverAddress by observable<InetAddress?>(null) { _, _, address -> + if (address != null) { + val addressString = address.addressString() + + input.setText(addressString) + input.setSelection(addressString.length) + } else { + input.setText("") + } + + input.requestFocus() + } + + init { + view.findViewById<View>(R.id.save).setOnClickListener { + adapter.saveDnsServer(input.text.toString()) { + input.apply { + setTextColor(errorColor) + addTextChangedListener(watcher) + } + } + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt b/android/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt new file mode 100644 index 0000000000..d310deb884 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt @@ -0,0 +1,10 @@ +package net.mullvad.talpid.util + +import java.net.InetAddress + +fun InetAddress.addressString(): String { + val hostNameAndAddress = this.toString().split('/', limit = 2) + val address = hostNameAndAddress[1] + + return address +} diff --git a/android/src/main/res/drawable/icon_add.xml b/android/src/main/res/drawable/icon_add.xml new file mode 100644 index 0000000000..f44a660a95 --- /dev/null +++ b/android/src/main/res/drawable/icon_add.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<rotate xmlns:android="http://schemas.android.com/apk/res/android" + android:fromDegrees="45" + android:toDegrees="45" + android:pivotX="50%" + android:pivotY="50%" + android:drawable="@drawable/icon_close" /> diff --git a/android/src/main/res/drawable/icon_tick_green.xml b/android/src/main/res/drawable/icon_tick_green.xml new file mode 100644 index 0000000000..a761a863ba --- /dev/null +++ b/android/src/main/res/drawable/icon_tick_green.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <group> + <path android:fillColor="@color/green" + android:pathData="M2.92646877,10.7979185 C2.25699855,10.1340272 1.17157288,10.1340272 0.502102661,10.7979185 C-0.167367554,11.4618098 -0.167367554,12.5381902 0.502102661,13.2020815 L7.35924552,20.0020815 C8.02871573,20.6659728 9.11414141,20.6659728 9.78361162,20.0020815 L23.4978973,6.40208153 C24.1673676,5.73819023 24.1673676,4.66180977 23.4978973,3.99791847 C22.8284271,3.33402718 21.7430014,3.33402718 21.0735312,3.99791847 L8.57142857,16.3958369 L2.92646877,10.7979185 Z" /> + </group> +</vector> diff --git a/android/src/main/res/layout/add_custom_dns_server.xml b/android/src/main/res/layout/add_custom_dns_server.xml new file mode 100644 index 0000000000..892b48a6fe --- /dev/null +++ b/android/src/main/res/layout/add_custom_dns_server.xml @@ -0,0 +1,31 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/blue40" + android:orientation="horizontal"> + <TextView android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_marginLeft="54dp" + android:layout_marginVertical="14dp" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:textColor="@color/white" + android:textSize="@dimen/text_medium" + android:text="@string/add_a_server" /> + <View android:id="@+id/click_area" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" + android:focusable="true" + android:clickable="true" + android:background="?android:attr/selectableItemBackground" /> + <ImageButton android:id="@+id/add" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="right" + android:paddingHorizontal="16dp" + android:paddingVertical="14dp" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/icon_add" /> +</FrameLayout> diff --git a/android/src/main/res/layout/advanced.xml b/android/src/main/res/layout/advanced.xml index c96c1b3d03..70359352e2 100644 --- a/android/src/main/res/layout/advanced.xml +++ b/android/src/main/res/layout/advanced.xml @@ -27,37 +27,9 @@ android:text="@string/settings_advanced" style="@style/SettingsCollapsedHeader" /> </FrameLayout> - <net.mullvad.mullvadvpn.ui.widget.ListenableScrollView android:id="@+id/scroll_area" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingBottom="@dimen/screen_vertical_margin" - android:orientation="vertical"> - <TextView android:id="@+id/expanded_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="2dp" - android:layout_marginLeft="@dimen/side_margin" - android:text="@string/settings_advanced" - style="@style/SettingsExpandedHeader" /> - <net.mullvad.mullvadvpn.ui.widget.MtuCell android:id="@+id/wireguard_mtu" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/vertical_space" - mullvad:text="@string/wireguard_mtu" /> - <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/wireguard_keys" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/vertical_space" - mullvad:text="@string/wireguard_key" /> - <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/split_tunneling" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="1dp" - mullvad:text="@string/split_tunneling" /> - </LinearLayout> - </net.mullvad.mullvadvpn.ui.widget.ListenableScrollView> + <net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView android:id="@+id/contents" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> </LinearLayout> </FrameLayout> diff --git a/android/src/main/res/layout/advanced_header.xml b/android/src/main/res/layout/advanced_header.xml new file mode 100644 index 0000000000..825e8285a4 --- /dev/null +++ b/android/src/main/res/layout/advanced_header.xml @@ -0,0 +1,34 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:mullvad="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="left"> + <TextView android:id="@+id/expanded_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="2dp" + android:layout_marginLeft="@dimen/side_margin" + android:text="@string/settings_advanced" + style="@style/SettingsExpandedHeader" /> + <net.mullvad.mullvadvpn.ui.widget.MtuCell android:id="@+id/wireguard_mtu" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/vertical_space" + mullvad:text="@string/wireguard_mtu" /> + <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/wireguard_keys" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/vertical_space" + mullvad:text="@string/wireguard_key" /> + <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/split_tunneling" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="1dp" + mullvad:text="@string/split_tunneling" /> + <net.mullvad.mullvadvpn.ui.widget.ToggleCell android:id="@+id/enable_custom_dns" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/vertical_space" + mullvad:text="@string/enable_custom_dns" /> +</LinearLayout> diff --git a/android/src/main/res/layout/custom_dns_footer.xml b/android/src/main/res/layout/custom_dns_footer.xml new file mode 100644 index 0000000000..c939eebb7f --- /dev/null +++ b/android/src/main/res/layout/custom_dns_footer.xml @@ -0,0 +1,14 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"> + <TextView android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingHorizontal="@dimen/cell_footer_horizontal_padding" + android:paddingBottom="@dimen/screen_vertical_margin" + android:paddingTop="@dimen/cell_footer_top_padding" + android:textColor="@color/white60" + android:textSize="@dimen/text_small" + android:text="@string/custom_dns_footer" /> +</FrameLayout> diff --git a/android/src/main/res/layout/custom_dns_server.xml b/android/src/main/res/layout/custom_dns_server.xml new file mode 100644 index 0000000000..54d7e9f01e --- /dev/null +++ b/android/src/main/res/layout/custom_dns_server.xml @@ -0,0 +1,31 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/blue40" + android:orientation="horizontal"> + <TextView android:id="@+id/label" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_marginLeft="54dp" + android:layout_marginVertical="14dp" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:textColor="@color/white" + android:textSize="@dimen/text_medium" /> + <View android:id="@+id/click_area" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" + android:focusable="true" + android:clickable="true" + android:background="?android:attr/selectableItemBackground" /> + <ImageButton android:id="@+id/remove" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="right" + android:paddingHorizontal="16dp" + android:paddingVertical="14dp" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/icon_close" /> +</FrameLayout> diff --git a/android/src/main/res/layout/edit_custom_dns_server.xml b/android/src/main/res/layout/edit_custom_dns_server.xml new file mode 100644 index 0000000000..91090efb9e --- /dev/null +++ b/android/src/main/res/layout/edit_custom_dns_server.xml @@ -0,0 +1,30 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/white" + android:orientation="horizontal"> + <EditText android:id="@+id/input" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:layout_marginLeft="54dp" + android:layout_marginVertical="14dp" + android:gravity="center_vertical" + android:background="@android:color/transparent" + android:singleLine="true" + android:imeOptions="flagNoPersonalizedLearning" + android:textCursorDrawable="@drawable/text_input_cursor" + android:textColorHint="@color/blue60" + android:textColor="@color/blue" + android:textSize="@dimen/text_medium" + android:hint="@string/custom_dns_example" /> + <ImageButton android:id="@+id/save" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_weight="0" + android:layout_gravity="right" + android:paddingHorizontal="16dp" + android:paddingVertical="14dp" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/icon_tick_green" /> +</LinearLayout> diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index f39e93bba0..fb442a9e9b 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -164,6 +164,10 @@ <string name="split_tunneling_description">Split tunneling makes it possible to select which applications should not be routed through the VPN tunnel.</string> <string name="enable">Enable</string> + <string name="enable_custom_dns">Use custom DNS server</string> + <string name="add_a_server">Add a server</string> + <string name="custom_dns_example">e.g. 123.456.789.111</string> + <string name="custom_dns_footer">Enable to add at least one DNS server.</string> <string name="exclude_applications">Exclude applications</string> <string name="account_url">https://mullvad.net/en/account</string> <string name="wg_key_url">https://mullvad.net/en/account/ports</string> |
