summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2020-07-15 11:04:00 -0300
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2020-07-15 11:04:00 -0300
commit489efcf777b673078a6762781e5de49d0ec5d85f (patch)
tree18db8f5240d7f923f2ab2ae832e096a303695d77
parente906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7 (diff)
parent2311a646a99452c24888ce4ee1ca0ea7a0ff7e78 (diff)
downloadmullvadvpn-489efcf777b673078a6762781e5de49d0ec5d85f.tar.xz
mullvadvpn-489efcf777b673078a6762781e5de49d0ec5d85f.zip
Merge branch 'android-split-tunnelling-ui'
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppInfo.kt8
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt69
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt70
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt7
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemDividerDecoration.kt (renamed from android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemDividerDecoration.kt)6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt3
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SplitTunnellingFragment.kt151
-rw-r--r--android/src/main/res/drawable/app_list_item_background.xml13
-rw-r--r--android/src/main/res/layout/advanced.xml23
-rw-r--r--android/src/main/res/layout/app_list_item.xml37
-rw-r--r--android/src/main/res/layout/split_tunnelling.xml55
-rw-r--r--android/src/main/res/layout/split_tunnelling_header.xml74
-rw-r--r--android/src/main/res/values/dimensions.xml2
-rw-r--r--android/src/main/res/values/strings.xml5
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>