summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/SettingsListener.kt18
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt5
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt220
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt65
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt9
-rw-r--r--android/src/main/res/drawable/cell_switch_background.xml27
-rw-r--r--android/src/main/res/layout/preferences.xml78
-rw-r--r--android/src/main/res/layout/settings.xml35
-rw-r--r--android/src/main/res/values/dimensions.xml6
-rw-r--r--android/src/main/res/values/strings.xml6
11 files changed, 470 insertions, 5 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/SettingsListener.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/SettingsListener.kt
index a822526890..be27eb7b89 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/SettingsListener.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/SettingsListener.kt
@@ -9,7 +9,8 @@ class SettingsListener(val daemon: MullvadDaemon) {
maybeSettings?.let { settings -> handleNewSettings(settings) }
}
- private var settings: Settings? = null
+ var settings: Settings? = null
+ private set
var onAccountNumberChange: ((String?) -> Unit)? = null
set(value) {
@@ -19,6 +20,17 @@ class SettingsListener(val daemon: MullvadDaemon) {
}
}
+ var onAllowLanChange: ((Boolean) -> Unit)? = null
+ set(value) {
+ synchronized(this) {
+ field = value
+
+ settings?.let { safeSettings ->
+ value?.invoke(safeSettings.allowLan)
+ }
+ }
+ }
+
var onRelaySettingsChange: ((RelaySettings?) -> Unit)? = null
set(value) {
synchronized(this) {
@@ -43,6 +55,10 @@ class SettingsListener(val daemon: MullvadDaemon) {
onRelaySettingsChange?.invoke(newSettings.relaySettings)
}
+ if (settings?.allowLan != newSettings.allowLan) {
+ onAllowLanChange?.invoke(newSettings.allowLan)
+ }
+
settings = newSettings
}
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt
index 9e4985820f..6cc83b157f 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt
@@ -1,3 +1,7 @@
package net.mullvad.mullvadvpn.model
-data class Settings(var accountToken: String?, var relaySettings: RelaySettings)
+data class Settings(
+ var accountToken: String?,
+ var relaySettings: RelaySettings,
+ var allowLan: Boolean
+)
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
index 8e8806cc70..e61567a0fe 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
@@ -81,6 +81,10 @@ class MullvadDaemon(val vpnService: MullvadVpnService) {
setAccount(daemonInterfaceAddress, accountToken)
}
+ fun setAllowLan(allowLan: Boolean) {
+ setAllowLan(daemonInterfaceAddress, allowLan)
+ }
+
fun shutdown() {
shutdown(daemonInterfaceAddress)
}
@@ -112,6 +116,7 @@ class MullvadDaemon(val vpnService: MullvadVpnService) {
private external fun getVersionInfo(daemonInterfaceAddress: Long): AppVersionInfo?
private external fun getWireguardKey(daemonInterfaceAddress: Long): PublicKey?
private external fun setAccount(daemonInterfaceAddress: Long, accountToken: String?)
+ private external fun setAllowLan(daemonInterfaceAddress: Long, allowLan: Boolean)
private external fun shutdown(daemonInterfaceAddress: Long)
private external fun updateRelaySettings(
daemonInterfaceAddress: Long,
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt
new file mode 100644
index 0000000000..0aba6ce947
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt
@@ -0,0 +1,220 @@
+package net.mullvad.mullvadvpn.ui
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Paint.Style
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.OvalShape
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.GestureDetector.OnGestureListener
+import android.view.Gravity
+import android.view.MotionEvent
+import android.widget.ImageView
+import android.widget.LinearLayout
+import net.mullvad.mullvadvpn.R
+
+class CellSwitch : LinearLayout {
+ enum class State {
+ ON,
+ OFF
+ }
+
+ var state = State.OFF
+ set(value) {
+ if (field != value) {
+ field = value
+ animateToState()
+ listener?.invoke(value)
+ }
+ }
+
+ var listener: ((State) -> Unit)? = null
+
+ private val onColor = resources.getColor(R.color.green)
+ private val offColor = resources.getColor(R.color.red)
+
+ private val knobSize = resources.getDimensionPixelSize(R.dimen.cell_switch_knob_size)
+ private val knobImage = ShapeDrawable(OvalShape()).apply {
+ paint.apply {
+ color = offColor
+ style = Style.FILL
+ }
+
+ intrinsicWidth = knobSize
+ intrinsicHeight = knobSize
+ }
+
+ private val knobView = ImageView(context).apply {
+ setImageDrawable(knobImage)
+ }
+
+ private val knobAnimationDuration = 200L
+ private val knobMaxTranslation =
+ resources.getDimensionPixelOffset(R.dimen.cell_switch_knob_max_translation).toFloat()
+
+ private val knobPosition: Float
+ get() = knobView.translationX / knobMaxTranslation
+
+ private val positionAnimation = ValueAnimator.ofFloat(0f, knobMaxTranslation).apply {
+ addUpdateListener { animation ->
+ knobView.translationX = animation.animatedValue as Float
+ }
+
+ duration = knobAnimationDuration
+ }
+
+ private val colorAnimation = ValueAnimator.ofArgb(offColor, onColor).apply {
+ addUpdateListener { animation ->
+ knobImage.paint.color = animation.animatedValue as Int
+ knobImage.invalidateSelf()
+ }
+
+ duration = knobAnimationDuration
+ }
+
+ private val gestureListener = object : OnGestureListener {
+ private var isScrolling: Boolean = false
+ private var scrollPosition: Float = 0f
+
+ override fun onDown(event: MotionEvent): Boolean {
+ scrollPosition = knobView.translationX
+ return true
+ }
+
+ override fun onFling(
+ downEvent: MotionEvent,
+ upEvent: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ if (velocityX > 0f) {
+ state = State.ON
+ } else if (velocityX < 0f) {
+ state = State.OFF
+ }
+
+ return true
+ }
+
+ override fun onLongPress(event: MotionEvent) {}
+
+ override fun onScroll(
+ downEvent: MotionEvent,
+ moveEvent: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ isScrolling = true
+ scrollPosition -= distanceX
+
+ var fraction = scrollPosition / knobMaxTranslation
+ val playTime = (fraction * knobAnimationDuration).toLong()
+
+ colorAnimation.pause()
+ positionAnimation.pause()
+
+ colorAnimation.currentPlayTime = playTime
+ positionAnimation.currentPlayTime = playTime
+
+ return true
+ }
+
+ override fun onShowPress(event: MotionEvent) {}
+
+ override fun onSingleTapUp(event: MotionEvent): Boolean {
+ when (state) {
+ State.ON -> state = State.OFF
+ State.OFF -> state = State.ON
+ }
+
+ return true
+ }
+
+ fun onUp(): Boolean {
+ if (!isScrolling) {
+ return false
+ }
+
+ if (knobPosition <= 0.5f) {
+ state = State.OFF
+ } else {
+ state = State.ON
+ }
+
+ isScrolling = false
+ scrollPosition = 0f
+
+ return true
+ }
+ }
+
+ private val gestureDetector = GestureDetector(context, gestureListener)
+
+ constructor(context: Context) : super(context) {}
+
+ constructor(context: Context, attributes: AttributeSet) : super(context, attributes) {}
+
+ constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) :
+ super(context, attributes, defaultStyleAttribute) {}
+
+ constructor(
+ context: Context,
+ attributes: AttributeSet,
+ defaultStyleAttribute: Int,
+ defaultStyleResource: Int
+ ) : super(context, attributes, defaultStyleAttribute, defaultStyleResource) {}
+
+ init {
+ setBackground(resources.getDrawable(R.drawable.cell_switch_background, null))
+ addView(knobView, LinearLayout.LayoutParams(knobSize, knobSize).apply {
+ gravity = Gravity.CENTER_VERTICAL
+ leftMargin = resources.getDimensionPixelSize(R.dimen.cell_switch_knob_margin)
+ })
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ if (gestureDetector.onTouchEvent(event)) {
+ return true
+ } else if (event.actionMasked == MotionEvent.ACTION_UP) {
+ return gestureListener.onUp()
+ }
+
+ return super.onTouchEvent(event)
+ }
+
+ fun forcefullySetState(newState: State) {
+ when (newState) {
+ State.ON -> {
+ knobView.translationX = knobMaxTranslation
+ knobImage.paint.color = onColor
+ }
+ State.OFF -> {
+ knobView.translationX = 0f
+ knobImage.paint.color = offColor
+ }
+ }
+
+ state = newState
+ }
+
+ private fun animateToState() {
+ var playTime = (knobPosition * knobAnimationDuration).toLong()
+
+ when (state) {
+ State.ON -> {
+ colorAnimation.start()
+ positionAnimation.start()
+ }
+ State.OFF -> {
+ colorAnimation.reverse()
+ positionAnimation.reverse()
+
+ playTime = knobAnimationDuration - playTime
+ }
+ }
+
+ colorAnimation.currentPlayTime = playTime
+ positionAnimation.currentPlayTime = playTime
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt
new file mode 100644
index 0000000000..160caafd22
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/PreferencesFragment.kt
@@ -0,0 +1,65 @@
+package net.mullvad.mullvadvpn.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.R
+
+class PreferencesFragment : ServiceDependentFragment(OnNoService.GoBack) {
+ private lateinit var allowLanToggle: CellSwitch
+
+ private var updateUiJob: Job? = null
+
+ override fun onSafelyCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = inflater.inflate(R.layout.preferences, container, false)
+
+ view.findViewById<View>(R.id.back).setOnClickListener {
+ parentActivity.onBackPressed()
+ }
+
+ allowLanToggle = view.findViewById<CellSwitch>(R.id.allow_lan_toggle).apply {
+ settingsListener.settings?.let { settings ->
+ if (settings.allowLan) {
+ forcefullySetState(CellSwitch.State.ON)
+ } else {
+ forcefullySetState(CellSwitch.State.OFF)
+ }
+ }
+
+ listener = { state ->
+ when (state) {
+ CellSwitch.State.ON -> daemon.setAllowLan(true)
+ CellSwitch.State.OFF -> daemon.setAllowLan(false)
+ }
+ }
+ }
+
+ settingsListener.onAllowLanChange = { allowLan ->
+ updateUiJob?.cancel()
+ updateUiJob = updateUi(allowLan)
+ }
+
+ return view
+ }
+
+ override fun onSafelyDestroyView() {
+ settingsListener.onAllowLanChange = null
+ }
+
+ private fun updateUi(allowLan: Boolean) = GlobalScope.launch(Dispatchers.Main) {
+ if (allowLan) {
+ allowLanToggle.state = CellSwitch.State.ON
+ } else {
+ allowLanToggle.state = CellSwitch.State.OFF
+ }
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt
index df68241819..154339b10a 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt
@@ -24,6 +24,7 @@ class SettingsFragment : ServiceAwareFragment() {
private lateinit var appVersionWarning: View
private lateinit var appVersionLabel: TextView
private lateinit var appVersionFooter: View
+ private lateinit var preferencesMenu: View
private lateinit var remainingTimeLabel: RemainingTimeLabel
private lateinit var wireguardKeysMenu: View
@@ -68,6 +69,13 @@ class SettingsFragment : ServiceAwareFragment() {
openSubFragment(AccountFragment())
}
}
+
+ preferencesMenu = view.findViewById<View>(R.id.preferences).apply {
+ setOnClickListener {
+ openSubFragment(PreferencesFragment())
+ }
+ }
+
wireguardKeysMenu = view.findViewById<View>(R.id.wireguard_keys).apply {
setOnClickListener {
openSubFragment(WireguardKeyFragment())
@@ -164,6 +172,7 @@ class SettingsFragment : ServiceAwareFragment() {
}
accountMenu.visibility = visibility
+ preferencesMenu.visibility = visibility
wireguardKeysMenu.visibility = visibility
}
diff --git a/android/src/main/res/drawable/cell_switch_background.xml b/android/src/main/res/drawable/cell_switch_background.xml
new file mode 100644
index 0000000000..71f6fe0802
--- /dev/null
+++ b/android/src/main/res/drawable/cell_switch_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle"
+ >
+ <item android:state_enabled="false">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/cell_switch_border_radius"/>
+ <stroke android:color="@color/white20" android:width="2dp"/>
+ <size
+ android:width="@dimen/cell_switch_width"
+ android:height="@dimen/cell_switch_height"
+ />
+ </shape>
+ </item>
+
+ <item android:state_enabled="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/cell_switch_border_radius"/>
+ <stroke android:color="@color/white" android:width="2dp"/>
+ <size
+ android:width="@dimen/cell_switch_width"
+ android:height="@dimen/cell_switch_height"
+ />
+ </shape>
+ </item>
+</selector>
diff --git a/android/src/main/res/layout/preferences.xml b/android/src/main/res/layout/preferences.xml
new file mode 100644
index 0000000000..59d42635fe
--- /dev/null
+++ b/android/src/main/res/layout/preferences.xml
@@ -0,0 +1,78 @@
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/darkBlue"
+ android:orientation="vertical"
+ android:gravity="left"
+ android:elevation="2dp"
+ >
+ <LinearLayout android:id="@+id/back"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ 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"
+ />
+ </LinearLayout>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginLeft="24dp"
+ android:textColor="@color/white"
+ android:textSize="32sp"
+ android:textStyle="bold"
+ android:text="@string/settings_preferences"
+ />
+ <LinearLayout android:id="@+id/allow_lan"
+ 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"
+ >
+ <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/local_network_sharing"
+ />
+ <net.mullvad.mullvadvpn.ui.CellSwitch android:id="@+id/allow_lan_toggle"
+ android:layout_width="52dp"
+ android:layout_height="32dp"
+ android:layout_weight="0"
+ />
+ </LinearLayout>
+ <TextView android:id="@+id/allow_lan_footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingHorizontal="24dp"
+ android:textColor="@color/white60"
+ android:textSize="13sp"
+ android:text="@string/allow_lan_footer"
+ />
+</LinearLayout>
diff --git a/android/src/main/res/layout/settings.xml b/android/src/main/res/layout/settings.xml
index 37cf3cb9db..c36c9bb3e8 100644
--- a/android/src/main/res/layout/settings.xml
+++ b/android/src/main/res/layout/settings.xml
@@ -19,7 +19,6 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginLeft="24dp"
- android:layout_marginBottom="24dp"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold"
@@ -28,7 +27,7 @@
<LinearLayout android:id="@+id/account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="24dp"
+ android:layout_marginTop="24dp"
android:paddingHorizontal="16dp"
android:background="@drawable/cell_button_background"
android:clickable="true"
@@ -66,10 +65,39 @@
android:src="@drawable/icon_chevron"
/>
</LinearLayout>
+ <LinearLayout android:id="@+id/preferences"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="1dp"
+ android:paddingHorizontal="16dp"
+ android:background="@drawable/cell_button_background"
+ android:clickable="true"
+ android:gravity="center"
+ android:visibility="gone"
+ >
+ <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/settings_preferences"
+ />
+ <ImageView
+ android:layout_width="14dp"
+ android:layout_height="24dp"
+ android:layout_weight="0"
+ android:alpha="0.6"
+ android:src="@drawable/icon_chevron"
+ />
+ </LinearLayout>
<LinearLayout android:id="@+id/wireguard_keys"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="24dp"
+ android:layout_marginTop="24dp"
android:paddingHorizontal="16dp"
android:background="@drawable/cell_button_background"
android:clickable="true"
@@ -98,6 +126,7 @@
<LinearLayout android:id="@+id/app_version"
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"
diff --git a/android/src/main/res/values/dimensions.xml b/android/src/main/res/values/dimensions.xml
index 800e1ed0ac..5d106210ea 100644
--- a/android/src/main/res/values/dimensions.xml
+++ b/android/src/main/res/values/dimensions.xml
@@ -6,4 +6,10 @@
<dimen name="account_input_corner_radius">4dp</dimen>
<dimen name="normal_button_height">44dp</dimen>
<dimen name="tall_button_height">64dp</dimen>
+ <dimen name="cell_switch_border_radius">16dp</dimen>
+ <dimen name="cell_switch_width">32dp</dimen>
+ <dimen name="cell_switch_height">52dp</dimen>
+ <dimen name="cell_switch_knob_margin">4dp</dimen>
+ <dimen name="cell_switch_knob_max_translation">20dp</dimen>
+ <dimen name="cell_switch_knob_size">24dp</dimen>
</resources>
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index d019f68b17..0a87ff3736 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
<string name="settings_account">Account</string>
<string name="less_than_a_day_left">less than a day left</string>
<string name="out_of_time">Out of time</string>
+ <string name="settings_preferences">Preferences</string>
<string name="app_version">App version</string>
<string name="update_available_footer">Update available, download to remain safe.</string>
<string name="report_a_problem">Report a problem</string>
@@ -40,6 +41,11 @@
<string name="paid_until">Paid until</string>
<string name="log_out">Log out</string>
+ <string name="local_network_sharing">Local network sharing</string>
+ <string name="allow_lan_footer">
+ Allows access to other devices on the same network for sharing, printing etc.
+ </string>
+
<string name="problem_report_description">
To help you more effectively, your app\'s log file will be attached to this message. Your
data will remain secure and private, as it is anonymised before being sent over an encrypted