diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-05-23 16:20:44 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2019-05-23 16:20:44 -0300 |
| commit | 40de1e01c0ebe0d6bf1183e9a8ecc956d15a0bb5 (patch) | |
| tree | 83354b998e1303baf62024c2f46044415a5fa200 | |
| parent | fb9711759ddad7a5b984dda8e65df4effe312500 (diff) | |
| parent | 074a68305b2a2f67ff6c0d3156f3490ef0aa006c (diff) | |
| download | mullvadvpn-40de1e01c0ebe0d6bf1183e9a8ecc956d15a0bb5.tar.xz mullvadvpn-40de1e01c0ebe0d6bf1183e9a8ecc956d15a0bb5.zip | |
Merge branch 'listen-for-tunnel-states-on-android'
11 files changed, 265 insertions, 92 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectActionButton.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectActionButton.kt index 4a577cd337..faf4a61521 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectActionButton.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectActionButton.kt @@ -3,15 +3,19 @@ package net.mullvad.mullvadvpn import android.view.View import android.widget.Button +import net.mullvad.mullvadvpn.model.TunnelStateTransition + class ConnectActionButton(val parentView: View) { private val button: Button = parentView.findViewById(R.id.action_button) - var state = ConnectionState.Disconnected + var state: TunnelStateTransition = TunnelStateTransition.Disconnected() set(value) { when (value) { - ConnectionState.Disconnected -> disconnected() - ConnectionState.Connecting -> connecting() - ConnectionState.Connected -> connected() + is TunnelStateTransition.Disconnected -> disconnected() + is TunnelStateTransition.Disconnecting -> disconnected() + is TunnelStateTransition.Connecting -> connecting() + is TunnelStateTransition.Connected -> connected() + is TunnelStateTransition.Blocked -> connected() } field = value @@ -27,9 +31,11 @@ class ConnectActionButton(val parentView: View) { private fun action() { when (state) { - ConnectionState.Disconnected -> onConnect?.invoke() - ConnectionState.Connecting -> onCancel?.invoke() - ConnectionState.Connected -> onDisconnect?.invoke() + is TunnelStateTransition.Disconnected -> onConnect?.invoke() + is TunnelStateTransition.Disconnecting -> onConnect?.invoke() + is TunnelStateTransition.Connecting -> onCancel?.invoke() + is TunnelStateTransition.Connected -> onDisconnect?.invoke() + is TunnelStateTransition.Blocked -> onDisconnect?.invoke() } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt index 8d7abfd24e..11bfabe912 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job import android.content.Context import android.os.Bundle @@ -14,24 +15,18 @@ import android.view.View import android.view.ViewGroup import android.widget.Button +import net.mullvad.mullvadvpn.model.TunnelStateTransition + class ConnectFragment : Fragment() { private lateinit var actionButton: ConnectActionButton private lateinit var headerBar: HeaderBar private lateinit var notificationBanner: NotificationBanner private lateinit var status: ConnectionStatus - private lateinit var connectHandler: Handler private lateinit var daemon: Deferred<MullvadDaemon> - private var state = ConnectionState.Disconnected - set(value) { - actionButton.state = value - headerBar.state = value - notificationBanner.state = value - status.state = value - - field = value - } + private var attachListenerJob: Job? = null + private var updateViewJob: Job? = null override fun onAttach(context: Context) { super.onAttach(context) @@ -39,12 +34,6 @@ class ConnectFragment : Fragment() { daemon = (context as MainActivity).asyncDaemon } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - connectHandler = Handler() - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -67,31 +56,40 @@ class ConnectFragment : Fragment() { onDisconnect = { disconnect() } } + attachListenerJob = attachListener() + return view } - private fun connect() { - state = ConnectionState.Connecting - GlobalScope.launch(Dispatchers.Default) { - daemon.await().connect() - } + override fun onDestroyView() { + attachListenerJob?.cancel() + detachListener() + updateViewJob?.cancel() + super.onDestroyView() + } - connectHandler.postDelayed(Runnable { connected() }, 1000) + private fun attachListener() = GlobalScope.launch(Dispatchers.Default) { + daemon.await().onTunnelStateChange = { state -> updateViewJob = updateView(state) } } - private fun disconnect() { - state = ConnectionState.Disconnected + private fun detachListener() = GlobalScope.launch(Dispatchers.Default) { + daemon.await().onTunnelStateChange = null + } - GlobalScope.launch(Dispatchers.Default) { - daemon.await().disconnect() - } + private fun connect() = GlobalScope.launch(Dispatchers.Default) { + daemon.await().connect() + } - connectHandler.removeCallbacksAndMessages(null) + private fun disconnect() = GlobalScope.launch(Dispatchers.Default) { + daemon.await().disconnect() } - private fun connected() { - state = ConnectionState.Connected + private fun updateView(state: TunnelStateTransition) = GlobalScope.launch(Dispatchers.Main) { + actionButton.state = state + headerBar.setState(state) + notificationBanner.setState(state) + status.setState(state) } private fun openSwitchLocationScreen() { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionState.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionState.kt deleted file mode 100644 index 4764ce05fe..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn - -enum class ConnectionState { - Disconnected, - Connecting, - Connected, -} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionStatus.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionStatus.kt index 7f608c5431..652eae4da5 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionStatus.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectionStatus.kt @@ -4,6 +4,8 @@ import android.content.Context import android.view.View import android.widget.TextView +import net.mullvad.mullvadvpn.model.TunnelStateTransition + class ConnectionStatus(val parentView: View, val context: Context) { private val spinner: View = parentView.findViewById(R.id.connecting_spinner) private val text: TextView = parentView.findViewById(R.id.connection_status) @@ -12,16 +14,15 @@ class ConnectionStatus(val parentView: View, val context: Context) { private val connectingTextColor = context.getColor(R.color.white) private val connectedTextColor = context.getColor(R.color.green) - var state = ConnectionState.Disconnected - set(value) { - when (value) { - ConnectionState.Disconnected -> disconnected() - ConnectionState.Connecting -> connecting() - ConnectionState.Connected -> connected() - } - - field = value + fun setState(state: TunnelStateTransition) { + when (state) { + is TunnelStateTransition.Disconnecting -> disconnected() + is TunnelStateTransition.Disconnected -> disconnected() + is TunnelStateTransition.Connecting -> connecting() + is TunnelStateTransition.Connected -> connected() + is TunnelStateTransition.Blocked -> connected() } + } private fun disconnected() { spinner.visibility = View.GONE diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/HeaderBar.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/HeaderBar.kt index 4c46174f51..6988dae839 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/HeaderBar.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/HeaderBar.kt @@ -3,22 +3,23 @@ package net.mullvad.mullvadvpn import android.content.Context import android.view.View +import net.mullvad.mullvadvpn.model.TunnelStateTransition + class HeaderBar(val parentView: View, val context: Context) { private val headerBar: View = parentView.findViewById(R.id.header_bar) private val securedColor = context.getColor(R.color.green) private val unsecuredColor = context.getColor(R.color.red) - var state = ConnectionState.Disconnected - set(value) { - when (value) { - ConnectionState.Disconnected -> unsecured() - ConnectionState.Connecting -> secured() - ConnectionState.Connected -> secured() - } - - field = value + fun setState(state: TunnelStateTransition) { + when (state) { + is TunnelStateTransition.Disconnected -> unsecured() + is TunnelStateTransition.Connecting -> secured() + is TunnelStateTransition.Connected -> secured() + is TunnelStateTransition.Disconnecting -> secured() + is TunnelStateTransition.Blocked -> secured() } + } private fun unsecured() { headerBar.setBackgroundColor(unsecuredColor) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt index e95a761ba5..80ac09eef2 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt @@ -4,6 +4,7 @@ import net.mullvad.mullvadvpn.model.AccountData import net.mullvad.mullvadvpn.model.RelayList import net.mullvad.mullvadvpn.model.RelaySettingsUpdate import net.mullvad.mullvadvpn.model.Settings +import net.mullvad.mullvadvpn.model.TunnelStateTransition class MullvadDaemon { init { @@ -11,6 +12,8 @@ class MullvadDaemon { initialize() } + var onTunnelStateChange: ((TunnelStateTransition) -> Unit)? = null + external fun connect() external fun disconnect() external fun getAccountData(accountToken: String): AccountData? @@ -20,4 +23,8 @@ class MullvadDaemon { external fun updateRelaySettings(update: RelaySettingsUpdate) private external fun initialize() + + private fun notifyTunnelStateEvent(event: TunnelStateTransition) { + onTunnelStateChange?.invoke(event) + } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/NotificationBanner.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/NotificationBanner.kt index 531e43f96f..7789d57435 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/NotificationBanner.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/NotificationBanner.kt @@ -2,20 +2,21 @@ package net.mullvad.mullvadvpn import android.view.View +import net.mullvad.mullvadvpn.model.TunnelStateTransition + class NotificationBanner(val parentView: View) { private val banner: View = parentView.findViewById(R.id.notification_banner) private var visible = false - var state = ConnectionState.Disconnected - set(value) { - when (value) { - ConnectionState.Disconnected -> hide() - ConnectionState.Connecting -> show() - ConnectionState.Connected -> hide() - } - - field = value + fun setState(state: TunnelStateTransition) { + when (state) { + is TunnelStateTransition.Disconnecting -> hide() + is TunnelStateTransition.Disconnected -> hide() + is TunnelStateTransition.Connecting -> show() + is TunnelStateTransition.Connected -> hide() + is TunnelStateTransition.Blocked -> show() } + } private fun show() { if (!visible) { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelStateTransition.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelStateTransition.kt new file mode 100644 index 0000000000..ec65037e42 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelStateTransition.kt @@ -0,0 +1,9 @@ +package net.mullvad.mullvadvpn.model + +sealed class TunnelStateTransition() { + class Disconnected() : TunnelStateTransition() + class Connecting() : TunnelStateTransition() + class Connected() : TunnelStateTransition() + class Disconnecting() : TunnelStateTransition() + class Blocked() : TunnelStateTransition() +} diff --git a/mullvad-jni/src/into_java.rs b/mullvad-jni/src/into_java.rs index 1c6733252b..743328dbff 100644 --- a/mullvad-jni/src/into_java.rs +++ b/mullvad-jni/src/into_java.rs @@ -12,6 +12,7 @@ use mullvad_types::{ CustomTunnelEndpoint, }; use std::fmt::Debug; +use talpid_types::tunnel::TunnelStateTransition; pub trait IntoJava<'env> { type JavaType; @@ -294,3 +295,25 @@ impl<'env> IntoJava<'env> for Settings { .expect("Failed to create Settings Java object") } } + +impl<'env> IntoJava<'env> for TunnelStateTransition { + type JavaType = JObject<'env>; + + fn into_java(self, env: &JNIEnv<'env>) -> Self::JavaType { + let variant = match self { + TunnelStateTransition::Disconnected => "Disconnected", + TunnelStateTransition::Connecting(_) => "Connecting", + TunnelStateTransition::Connected(_) => "Connected", + TunnelStateTransition::Disconnecting(_) => "Disconnecting", + TunnelStateTransition::Blocked(_) => "Blocked", + }; + + let class = get_class(&format!( + "net/mullvad/mullvadvpn/model/TunnelStateTransition${}", + variant + )); + + env.new_object(&class, "()V", &[]) + .expect("Failed to create TunnelStateTransition sub-class variant Java object") + } +} diff --git a/mullvad-jni/src/jni_event_listener.rs b/mullvad-jni/src/jni_event_listener.rs new file mode 100644 index 0000000000..7beb7ae844 --- /dev/null +++ b/mullvad-jni/src/jni_event_listener.rs @@ -0,0 +1,123 @@ +use crate::{get_class, into_java::IntoJava}; +use jni::{ + objects::{JMethodID, JObject, JValue}, + signature::{JavaType, Primitive}, + AttachGuard, JNIEnv, +}; +use mullvad_daemon::EventListener; +use mullvad_types::{relay_list::RelayList, settings::Settings}; +use std::{sync::mpsc, thread}; +use talpid_types::{tunnel::TunnelStateTransition, ErrorExt}; + +#[derive(Debug, err_derive::Error)] +pub enum Error { + #[error(display = "Failed to create global reference to MullvadDaemon Java object")] + CreateGlobalReference(#[error(cause)] jni::errors::Error), + + #[error(display = "Failed to find {} method", _0)] + FindMethod(&'static str, #[error(cause)] jni::errors::Error), + + #[error(display = "Failed to retrieve Java VM instance")] + GetJvmInstance(#[error(cause)] jni::errors::Error), +} + +#[derive(Clone, Debug)] +pub struct JniEventListener(mpsc::Sender<TunnelStateTransition>); + +impl JniEventListener { + pub fn spawn(env: &JNIEnv, mullvad_daemon: &JObject) -> Result<Self, Error> { + JniEventHandler::spawn(env, mullvad_daemon) + } +} + +impl EventListener for JniEventListener { + fn notify_new_state(&self, state: TunnelStateTransition) { + let _ = self.0.send(state); + } + + fn notify_settings(&self, _: Settings) {} + fn notify_relay_list(&self, _: RelayList) {} +} + +struct JniEventHandler<'env> { + env: AttachGuard<'env>, + mullvad_ipc_client: JObject<'env>, + notify_tunnel_event: JMethodID<'env>, + events: mpsc::Receiver<TunnelStateTransition>, +} + +impl JniEventHandler<'_> { + pub fn spawn( + old_env: &JNIEnv, + old_mullvad_ipc_client: &JObject, + ) -> Result<JniEventListener, Error> { + let (tx, rx) = mpsc::channel(); + let jvm = old_env.get_java_vm().map_err(Error::GetJvmInstance)?; + let mullvad_ipc_client = old_env + .new_global_ref(*old_mullvad_ipc_client) + .map_err(Error::CreateGlobalReference)?; + + thread::spawn(move || match jvm.attach_current_thread() { + Ok(env) => match JniEventHandler::new(env, mullvad_ipc_client.as_obj(), rx) { + Ok(mut listener) => listener.run(), + Err(error) => log::error!("{}", error.display_chain()), + }, + Err(error) => { + log::error!( + "{}", + error.display_chain_with_msg( + "Failed to attach tunnel event listener thread to Java VM" + ) + ); + } + }); + + Ok(JniEventListener(tx)) + } +} + +impl<'env> JniEventHandler<'env> { + fn new( + env: AttachGuard<'env>, + mullvad_ipc_client: JObject<'env>, + events: mpsc::Receiver<TunnelStateTransition>, + ) -> Result<Self, Error> { + let class = get_class("net/mullvad/mullvadvpn/MullvadDaemon"); + let notify_tunnel_event = env + .get_method_id( + &class, + "notifyTunnelStateEvent", + "(Lnet/mullvad/mullvadvpn/model/TunnelStateTransition;)V", + ) + .map_err(|error| Error::FindMethod("notifyTunnelStateEvent", error))?; + + Ok(JniEventHandler { + env, + mullvad_ipc_client, + notify_tunnel_event, + events, + }) + } + + fn run(&mut self) { + while let Ok(event) = self.events.recv() { + self.handle_tunnel_event(event); + } + } + + fn handle_tunnel_event(&self, event: TunnelStateTransition) { + let result = self.env.call_method_unchecked( + self.mullvad_ipc_client, + self.notify_tunnel_event, + JavaType::Primitive(Primitive::Void), + &[JValue::Object(event.into_java(&self.env))], + ); + + if let Err(error) = result { + log::error!( + "{}", + error.display_chain_with_msg("Failed to call MullvadDaemon.notifyTunnelStateEvent") + ); + } + } +} diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index d5708c9a0a..83e60e0486 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -4,18 +4,21 @@ mod daemon_interface; mod from_java; mod into_java; mod is_null; +mod jni_event_listener; -use crate::{daemon_interface::DaemonInterface, from_java::FromJava, into_java::IntoJava}; +use crate::{ + daemon_interface::DaemonInterface, from_java::FromJava, into_java::IntoJava, + jni_event_listener::JniEventListener, +}; use jni::{ objects::{GlobalRef, JObject, JString}, JNIEnv, }; use lazy_static::lazy_static; -use mullvad_daemon::{logging, version, Daemon, DaemonCommandSender, EventListener}; -use mullvad_types::{relay_list::RelayList, settings::Settings}; +use mullvad_daemon::{logging, version, Daemon, DaemonCommandSender}; use parking_lot::{Mutex, RwLock}; use std::{collections::HashMap, path::PathBuf, sync::mpsc, thread}; -use talpid_types::{tunnel::TunnelStateTransition, ErrorExt}; +use talpid_types::ErrorExt; const LOG_FILENAME: &str = "daemon.log"; @@ -36,6 +39,12 @@ const CLASSES_TO_LOAD: &[&str] = &[ "net/mullvad/mullvadvpn/model/RelaySettingsUpdate$CustomTunnelEndpoint", "net/mullvad/mullvadvpn/model/RelaySettingsUpdate$RelayConstraintsUpdate", "net/mullvad/mullvadvpn/model/Settings", + "net/mullvad/mullvadvpn/model/TunnelStateTransition$Blocked", + "net/mullvad/mullvadvpn/model/TunnelStateTransition$Connected", + "net/mullvad/mullvadvpn/model/TunnelStateTransition$Connecting", + "net/mullvad/mullvadvpn/model/TunnelStateTransition$Disconnected", + "net/mullvad/mullvadvpn/model/TunnelStateTransition$Disconnecting", + "net/mullvad/mullvadvpn/MullvadDaemon", ]; lazy_static! { @@ -51,19 +60,22 @@ pub enum Error { #[error(display = "Failed to initialize the mullvad daemon")] InitializeDaemon(#[error(cause)] mullvad_daemon::Error), + + #[error(display = "Failed to spawn the JNI event listener")] + SpawnJniEventListener(#[error(cause)] jni_event_listener::Error), } #[no_mangle] #[allow(non_snake_case)] pub extern "system" fn Java_net_mullvad_mullvadvpn_MullvadDaemon_initialize( env: JNIEnv, - _: JObject, + this: JObject, ) { let log_dir = start_logging(); load_classes(&env); - if let Err(error) = initialize(log_dir) { + if let Err(error) = initialize(&env, &this, log_dir) { log::error!("{}", error.display_chain()); } } @@ -97,8 +109,8 @@ fn load_class_reference(env: &JNIEnv, name: &str) -> GlobalRef { .expect("Failed to convert local reference to Java class into a global reference") } -fn initialize(log_dir: PathBuf) -> Result<(), Error> { - let daemon_command_sender = spawn_daemon(log_dir)?; +fn initialize(env: &JNIEnv, this: &JObject, log_dir: PathBuf) -> Result<(), Error> { + let daemon_command_sender = spawn_daemon(env, this, log_dir)?; DAEMON_INTERFACE .lock() @@ -107,10 +119,15 @@ fn initialize(log_dir: PathBuf) -> Result<(), Error> { Ok(()) } -fn spawn_daemon(log_dir: PathBuf) -> Result<DaemonCommandSender, Error> { +fn spawn_daemon( + env: &JNIEnv, + this: &JObject, + log_dir: PathBuf, +) -> Result<DaemonCommandSender, Error> { + let listener = JniEventListener::spawn(env, this).map_err(Error::SpawnJniEventListener)?; let (tx, rx) = mpsc::channel(); - thread::spawn(move || match create_daemon(log_dir) { + thread::spawn(move || match create_daemon(listener, log_dir) { Ok(daemon) => { let _ = tx.send(Ok(daemon.command_sender())); match daemon.run() { @@ -126,12 +143,15 @@ fn spawn_daemon(log_dir: PathBuf) -> Result<DaemonCommandSender, Error> { rx.recv().unwrap() } -fn create_daemon(log_dir: PathBuf) -> Result<Daemon<DummyListener>, Error> { +fn create_daemon( + listener: JniEventListener, + log_dir: PathBuf, +) -> Result<Daemon<JniEventListener>, Error> { let resource_dir = mullvad_paths::get_resource_dir(); let cache_dir = mullvad_paths::cache_dir().map_err(Error::GetCacheDir)?; let daemon = Daemon::start_with_event_listener( - DummyListener, + listener, Some(log_dir), resource_dir, cache_dir, @@ -142,15 +162,6 @@ fn create_daemon(log_dir: PathBuf) -> Result<Daemon<DummyListener>, Error> { Ok(daemon) } -#[derive(Clone, Copy, Debug)] -struct DummyListener; - -impl EventListener for DummyListener { - fn notify_new_state(&self, _: TunnelStateTransition) {} - fn notify_settings(&self, _: Settings) {} - fn notify_relay_list(&self, _: RelayList) {} -} - fn get_class(name: &str) -> GlobalRef { match CLASSES.read().get(name) { Some(class) => class.clone(), |
