diff options
Diffstat (limited to 'android')
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 |
