diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-07-15 11:04:00 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-07-15 11:04:00 -0300 |
| commit | 489efcf777b673078a6762781e5de49d0ec5d85f (patch) | |
| tree | 18db8f5240d7f923f2ab2ae832e096a303695d77 | |
| parent | e906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7 (diff) | |
| parent | 2311a646a99452c24888ce4ee1ca0ea7a0ff7e78 (diff) | |
| download | mullvadvpn-489efcf777b673078a6762781e5de49d0ec5d85f.tar.xz mullvadvpn-489efcf777b673078a6762781e5de49d0ec5d85f.zip | |
Merge branch 'android-split-tunnelling-ui'
15 files changed, 521 insertions, 6 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppInfo.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppInfo.kt new file mode 100644 index 0000000000..f7b7986993 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppInfo.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.applist + +import android.content.pm.ApplicationInfo +import android.graphics.drawable.Drawable + +data class AppInfo(val info: ApplicationInfo, val label: String) { + var icon: Drawable? = null +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt new file mode 100644 index 0000000000..fe9c223194 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt @@ -0,0 +1,69 @@ +package net.mullvad.mullvadvpn.applist + +import android.content.Context +import android.support.v7.widget.RecyclerView.Adapter +import android.view.LayoutInflater +import android.view.ViewGroup +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.util.JobTracker + +class AppListAdapter(context: Context) : Adapter<AppListItemHolder>() { + private val appList = ArrayList<AppInfo>() + private val jobTracker = JobTracker() + private val packageManager = context.packageManager + private val thisPackageName = context.packageName + + var onListReady: (suspend () -> Unit)? = null + + var isListReady = false + private set + + var enabled by observable(false) { _, oldValue, newValue -> + if (oldValue != newValue) { + if (newValue == true) { + notifyItemRangeInserted(0, appList.size) + } else { + notifyItemRangeRemoved(0, appList.size) + } + } + } + + init { + jobTracker.newBackgroundJob("populateAppList") { + populateAppList(context) + } + } + + override fun getItemCount() = if (enabled) { appList.size } else { 0 } + + override fun onCreateViewHolder(parentView: ViewGroup, type: Int): AppListItemHolder { + val inflater = LayoutInflater.from(parentView.context) + val view = inflater.inflate(R.layout.app_list_item, parentView, false) + + return AppListItemHolder(packageManager, jobTracker, view) + } + + override fun onBindViewHolder(holder: AppListItemHolder, position: Int) { + holder.appInfo = appList.get(position) + } + + private fun populateAppList(context: Context) { + val applications = packageManager + .getInstalledApplications(0) + .filter { info -> info.packageName != thisPackageName } + .map { info -> AppInfo(info, packageManager.getApplicationLabel(info).toString()) } + + appList.apply { + clear() + addAll(applications) + sortBy { info -> info.label } + } + + jobTracker.newUiJob("notifyAppListChanges") { + isListReady = true + onListReady?.invoke() + notifyItemRangeInserted(0, applications.size) + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt new file mode 100644 index 0000000000..04bdd55686 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt @@ -0,0 +1,70 @@ +package net.mullvad.mullvadvpn.applist + +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.ui.CellSwitch +import net.mullvad.mullvadvpn.util.JobTracker + +class AppListItemHolder( + private val packageManager: PackageManager, + private val jobTracker: JobTracker, + view: View +) : ViewHolder(view) { + private val loading: View = view.findViewById(R.id.loading) + private val icon: ImageView = view.findViewById(R.id.icon) + private val name: TextView = view.findViewById(R.id.name) + private val excluded: CellSwitch = view.findViewById(R.id.excluded) + + var appInfo by observable<AppInfo?>(null) { _, _, info -> + if (info != null) { + val iconImage = info.icon + + name.text = info.label + + if (iconImage != null) { + showIcon(iconImage) + } else { + hideIcon() + loadIcon(info) + } + } else { + name.text = "" + hideIcon() + } + } + + init { + view.setOnClickListener { + excluded.toggle() + } + } + + private fun hideIcon() { + icon.visibility = View.GONE + loading.visibility = View.VISIBLE + } + + private fun showIcon(iconImage: Drawable) { + loading.visibility = View.GONE + icon.setImageDrawable(iconImage) + icon.visibility = View.VISIBLE + } + + private fun loadIcon(info: AppInfo) { + jobTracker.newUiJob("load icon for ${info.info.packageName}") { + val iconImage = jobTracker.runOnBackground { + packageManager.getApplicationIcon(info.info) + } + + info.icon = iconImage + + showIcon(iconImage) + } + } +} 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 4f51012759..ba9ca142d8 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt @@ -47,6 +47,10 @@ class AdvancedFragment : ServiceDependentFragment(OnNoService.GoBack) { } } + view.findViewById<View>(R.id.split_tunnelling).setOnClickListener { + openSubFragment(SplitTunnellingFragment()) + } + settingsListener.subscribe(this) { settings -> updateUi(settings) } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt index a012087e61..b01a73b3e3 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt @@ -186,6 +186,13 @@ class CellSwitch : LinearLayout { return super.onTouchEvent(event) } + fun toggle() { + when (state) { + State.ON -> state = State.OFF + State.OFF -> state = State.ON + } + } + fun forcefullySetState(newState: State) { when (newState) { State.ON -> { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemDividerDecoration.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemDividerDecoration.kt index a1fbb8983f..26dcebe86d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemDividerDecoration.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemDividerDecoration.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.relaylist +package net.mullvad.mullvadvpn.ui import android.content.Context import android.graphics.Rect @@ -8,8 +8,8 @@ import android.support.v7.widget.RecyclerView.State import android.view.View import net.mullvad.mullvadvpn.R -class RelayItemDividerDecoration(private val context: Context) : ItemDecoration() { - private val dividerHeight = context.resources.getDimensionPixelSize(R.dimen.relay_list_divider) +class ListItemDividerDecoration(private val context: Context) : ItemDecoration() { + private val dividerHeight = context.resources.getDimensionPixelSize(R.dimen.list_item_divider) override fun getItemOffsets(offsets: Rect, view: View, parent: RecyclerView, state: State) { val position = parent.getChildAdapterPosition(view) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt index 1ba8948375..a920e96a93 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt @@ -18,7 +18,6 @@ import net.mullvad.mullvadvpn.model.LocationConstraint import net.mullvad.mullvadvpn.model.RelayConstraintsUpdate import net.mullvad.mullvadvpn.model.RelaySettingsUpdate import net.mullvad.mullvadvpn.relaylist.RelayItem -import net.mullvad.mullvadvpn.relaylist.RelayItemDividerDecoration import net.mullvad.mullvadvpn.relaylist.RelayList import net.mullvad.mullvadvpn.relaylist.RelayListAdapter import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView @@ -75,7 +74,7 @@ class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchSc } } - addItemDecoration(RelayItemDividerDecoration(parentActivity)) + addItemDecoration(ListItemDividerDecoration(parentActivity)) } return view diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SplitTunnellingFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SplitTunnellingFragment.kt new file mode 100644 index 0000000000..e00bda1d5e --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SplitTunnellingFragment.kt @@ -0,0 +1,151 @@ +package net.mullvad.mullvadvpn.ui + +import android.animation.Animator +import android.animation.Animator.AnimatorListener +import android.animation.ObjectAnimator +import android.content.Context +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.applist.AppListAdapter +import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView +import net.mullvad.mullvadvpn.util.AdapterWithHeader + +class SplitTunnellingFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { + private val excludeApplicationsFadeOutListener = object : AnimatorListener { + override fun onAnimationCancel(animation: Animator) {} + override fun onAnimationRepeat(animation: Animator) {} + override fun onAnimationStart(animation: Animator) {} + + override fun onAnimationEnd(animation: Animator) { + if (!appListAdapter.enabled && appListAdapter.isListReady) { + excludeApplications.visibility = View.GONE + } + } + } + + private val loadingSpinnerFadeOutListener = object : AnimatorListener { + override fun onAnimationCancel(animation: Animator) {} + override fun onAnimationRepeat(animation: Animator) {} + override fun onAnimationStart(animation: Animator) {} + + override fun onAnimationEnd(animation: Animator) { + if (appListAdapter.isListReady) { + appListAdapter.enabled = true + loadingSpinner.visibility = View.GONE + } + } + } + + private lateinit var appListAdapter: AppListAdapter + private lateinit var enabledToggle: CellSwitch + private lateinit var excludeApplicationsFadeOut: ObjectAnimator + private lateinit var loadingSpinnerFadeIn: ObjectAnimator + private lateinit var titleController: CollapsibleTitleController + + private lateinit var excludeApplications: View + private lateinit var loadingSpinner: View + + override fun onAttach(context: Context) { + super.onAttach(context) + + appListAdapter = AppListAdapter(context) + } + + override fun onSafelyCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.split_tunnelling, container, false) + + view.findViewById<View>(R.id.back).setOnClickListener { + activity?.onBackPressed() + } + + titleController = CollapsibleTitleController(view, R.id.app_list) + + view.findViewById<CustomRecyclerView>(R.id.app_list).apply { + layoutManager = LinearLayoutManager(parentActivity) + + adapter = AdapterWithHeader(appListAdapter, R.layout.split_tunnelling_header).apply { + onHeaderAvailable = { headerView -> + configureHeader(headerView) + titleController.expandedTitleView = headerView.findViewById(R.id.expanded_title) + } + } + + addItemDecoration(ListItemDividerDecoration(parentActivity)) + } + + return view + } + + override fun onSafelyDestroyView() { + titleController.onDestroy() + } + + private fun configureHeader(header: View) { + excludeApplications = header.findViewById(R.id.exclude_applications) + loadingSpinner = header.findViewById(R.id.loading_spinner) + + excludeApplicationsFadeOut = + ObjectAnimator.ofFloat(excludeApplications, "alpha", 1.0f, 0.0f).apply { + addListener(excludeApplicationsFadeOutListener) + setDuration(200) + } + + loadingSpinnerFadeIn = + ObjectAnimator.ofFloat(loadingSpinner, "alpha", 0.0f, 1.0f).apply { + addListener(loadingSpinnerFadeOutListener) + setDuration(200) + } + + enabledToggle = header.findViewById<CellSwitch>(R.id.enabled_toggle).apply { + listener = { toggleState -> + when (toggleState) { + CellSwitch.State.ON -> enable() + CellSwitch.State.OFF -> disable() + } + } + } + + header.findViewById<View>(R.id.enabled).setOnClickListener { + enabledToggle.toggle() + } + } + + private fun enable() { + appListAdapter.apply { + if (!isListReady) { + enabled = false + showLoadingSpinner() + onListReady = { + hideLoadingSpinner() + } + } else { + enabled = true + } + } + + excludeApplications.visibility = View.VISIBLE + excludeApplicationsFadeOut.reverse() + } + + private fun disable() { + appListAdapter.enabled = false + excludeApplicationsFadeOut.start() + } + + private fun showLoadingSpinner() { + loadingSpinner.visibility = View.VISIBLE + loadingSpinnerFadeIn.start() + } + + private fun hideLoadingSpinner() { + loadingSpinnerFadeIn.reverse() + } +} diff --git a/android/src/main/res/drawable/app_list_item_background.xml b/android/src/main/res/drawable/app_list_item_background.xml new file mode 100644 index 0000000000..9cde2e4032 --- /dev/null +++ b/android/src/main/res/drawable/app_list_item_background.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="false"> + <shape android:shape="rectangle"> + <solid android:color="@color/blue40" /> + </shape> + </item> + <item android:state_pressed="true"> + <shape android:shape="rectangle"> + <solid android:color="@color/blue60" /> + </shape> + </item> +</selector> diff --git a/android/src/main/res/layout/advanced.xml b/android/src/main/res/layout/advanced.xml index 990784ac80..3d24a40d03 100644 --- a/android/src/main/res/layout/advanced.xml +++ b/android/src/main/res/layout/advanced.xml @@ -125,6 +125,29 @@ android:alpha="0.6" android:src="@drawable/icon_chevron" /> </LinearLayout> + <LinearLayout android:id="@+id/split_tunnelling" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:paddingHorizontal="16dp" + android:background="@drawable/cell_button_background" + android:clickable="true" + android:gravity="center"> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="@string/split_tunnelling" /> + <ImageView android:layout_width="14dp" + android:layout_height="24dp" + android:layout_weight="0" + android:alpha="0.6" + android:src="@drawable/icon_chevron" /> + </LinearLayout> </LinearLayout> </net.mullvad.mullvadvpn.ui.widget.ListenableScrollView> </LinearLayout> diff --git a/android/src/main/res/layout/app_list_item.xml b/android/src/main/res/layout/app_list_item.xml new file mode 100644 index 0000000000..741f3220de --- /dev/null +++ b/android/src/main/res/layout/app_list_item.xml @@ -0,0 +1,37 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="16dp" + android:background="@drawable/app_list_item_background" + android:orientation="horizontal" + android:gravity="center" + android:clickable="true"> + <ProgressBar android:id="@+id/loading" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="center" + android:indeterminate="true" + android:indeterminateOnly="true" + android:indeterminateDuration="600" + android:indeterminateDrawable="@drawable/icon_spinner" + android:visibility="visible" /> + <ImageView android:id="@+id/icon" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="center" + android:layout_marginLeft="8dp" + android:visibility="gone" /> + <TextView android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginHorizontal="8dp" + android:layout_marginVertical="16dp" + android:textColor="@color/white" + android:textSize="16sp" + android:text="" /> + <net.mullvad.mullvadvpn.ui.CellSwitch android:id="@+id/excluded" + android:layout_width="52dp" + android:layout_height="32dp" + android:layout_weight="0" /> +</LinearLayout> diff --git a/android/src/main/res/layout/split_tunnelling.xml b/android/src/main/res/layout/split_tunnelling.xml new file mode 100644 index 0000000000..e6284884c5 --- /dev/null +++ b/android/src/main/res/layout/split_tunnelling.xml @@ -0,0 +1,55 @@ +<FrameLayout 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="match_parent" + android:background="@color/darkBlue" + android:elevation="3dp" + android:gravity="left"> + <TextView android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + android:text="@string/split_tunnelling" /> + <LinearLayout android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <FrameLayout android:layout_width="match_parent" + android:layout_height="wrap_content"> + <LinearLayout android:id="@+id/back" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:padding="12dp" + android:orientation="horizontal" + android:gravity="center_vertical | left" + android:clickable="true" + android:background="?android:attr/selectableItemBackground"> + <ImageView android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginRight="8dp" + android:src="@drawable/icon_back" /> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white60" + android:textSize="13sp" + android:textStyle="bold" + android:text="@string/settings_advanced" /> + </LinearLayout> + <TextView android:id="@+id/collapsed_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="4dp" + android:layout_gravity="center" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + android:text="@string/split_tunnelling" /> + </FrameLayout> + <net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView android:id="@+id/app_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> + </LinearLayout> +</FrameLayout> diff --git a/android/src/main/res/layout/split_tunnelling_header.xml b/android/src/main/res/layout/split_tunnelling_header.xml new file mode 100644 index 0000000000..4db5c6fc80 --- /dev/null +++ b/android/src/main/res/layout/split_tunnelling_header.xml @@ -0,0 +1,74 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + 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_weight="0" + android:layout_marginLeft="24dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="12dp" + android:text="@string/split_tunnelling" + android:textColor="@color/white" + android:textSize="32sp" + android:textStyle="bold" /> + <TextView android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingHorizontal="24dp" + android:text="@string/split_tunnelling_description" + android:textColor="@color/white60" + android:textSize="13sp" /> + <LinearLayout android:id="@+id/enabled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:paddingHorizontal="16dp" + android:background="@drawable/cell_button_background" + android:gravity="center" + android:clickable="true"> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="@string/enabled" /> + <net.mullvad.mullvadvpn.ui.CellSwitch android:id="@+id/enabled_toggle" + android:layout_width="52dp" + android:layout_height="32dp" + android:layout_weight="0" /> + </LinearLayout> + <LinearLayout android:id="@+id/exclude_applications" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:paddingHorizontal="16dp" + android:background="@drawable/cell_button_background" + android:visibility="gone" + android:gravity="center"> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="8dp" + android:paddingVertical="17dp" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + android:text="@string/exclude_applications" /> + </LinearLayout> + <ProgressBar android:id="@+id/loading_spinner" + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_gravity="center" + android:layout_marginTop="24dp" + android:indeterminate="true" + android:indeterminateOnly="true" + android:indeterminateDuration="600" + android:indeterminateDrawable="@drawable/icon_spinner" + android:visibility="gone" /> +</LinearLayout> diff --git a/android/src/main/res/values/dimensions.xml b/android/src/main/res/values/dimensions.xml index 4342bec6c5..197cb92ecb 100644 --- a/android/src/main/res/values/dimensions.xml +++ b/android/src/main/res/values/dimensions.xml @@ -2,7 +2,7 @@ <dimen name="country_row_padding">20dp</dimen> <dimen name="city_row_padding">40dp</dimen> <dimen name="relay_row_padding">60dp</dimen> - <dimen name="relay_list_divider">1dp</dimen> + <dimen name="list_item_divider">1dp</dimen> <dimen name="dialog_margin">14dp</dimen> <dimen name="account_input_corner_radius">4dp</dimen> <dimen name="edit_text_corner_radius">4dp</dimen> diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 5160671d22..f25e782df6 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -159,6 +159,11 @@ <string name="wireguard_key_verification_failure">Key verification failed</string> <string name="wireguard_public_key">WireGuard public key</string> <string name="copied_wireguard_public_key">Copied WireGuard public key to clipboard</string> + <string name="split_tunnelling">Split tunnelling</string> + <string name="split_tunnelling_description">Split tunnelling makes it possible to select which + applications should not be routed through the VPN tunnel.</string> + <string name="enabled">Enabled</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/account/ports</string> <string name="create_account_url">https://mullvad.net/en/account/create</string> |
