summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2019-08-29 14:08:59 -0300
committerJanito Vaqueiro Ferreira Filho <janito@mullvad.net>2019-08-29 14:08:59 -0300
commitcab99e295609afa3c2933194640d2d98ac7b7ffa (patch)
tree0e4d975dadc1b32e9b0f74952807d43db57df999 /android
parent454e6893985b010a8ca2c07d288d44995348bd48 (diff)
parent42b6948f2b3bf675c2172ddb04ba12781b944421 (diff)
downloadmullvadvpn-cab99e295609afa3c2933194640d2d98ac7b7ffa.tar.xz
mullvadvpn-cab99e295609afa3c2933194640d2d98ac7b7ffa.zip
Merge branch 'foreground-notification'
Diffstat (limited to 'android')
-rw-r--r--android/proguard-rules.pro1
-rw-r--r--android/src/main/AndroidManifest.xml1
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt47
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ForegroundNotificationManager.kt100
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt48
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt10
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt5
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt66
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/ConnectionProxy.kt45
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/util/EventNotifier.kt42
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt46
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt32
-rw-r--r--android/src/main/res/drawable-hdpi/notification.pngbin0 -> 755 bytes
-rw-r--r--android/src/main/res/drawable-mdpi/notification.pngbin0 -> 505 bytes
-rw-r--r--android/src/main/res/drawable-xhdpi/notification.pngbin0 -> 1000 bytes
-rw-r--r--android/src/main/res/drawable-xxhdpi/notification.pngbin0 -> 1570 bytes
-rw-r--r--android/src/main/res/drawable-xxxhdpi/notification.pngbin0 -> 2143 bytes
-rw-r--r--android/src/main/res/values/strings.xml7
18 files changed, 367 insertions, 83 deletions
diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro
index a50d295303..30ff963a19 100644
--- a/android/proguard-rules.pro
+++ b/android/proguard-rules.pro
@@ -1,4 +1,5 @@
-dontwarn org.joda.convert.**
+-dontwarn net.mullvad.mullvadvpn.ConnectFragment$onCreateView$2$?
-keep class net.mullvad.mullvadvpn.model.** { *; }
-keep class net.mullvad.mullvadvpn.MullvadDaemon { *; }
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 9dbdab3612..5cacbd46b2 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
package="net.mullvad.mullvadvpn"
>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt
index 1e7f16e94d..7e74c53066 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ConnectFragment.kt
@@ -18,6 +18,7 @@ import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy
import net.mullvad.mullvadvpn.dataproxy.KeyStatusListener
import net.mullvad.mullvadvpn.dataproxy.LocationInfoCache
import net.mullvad.mullvadvpn.dataproxy.RelayListListener
+import net.mullvad.mullvadvpn.util.SmartDeferred
import net.mullvad.mullvadvpn.model.KeygenEvent
import net.mullvad.mullvadvpn.model.TunnelState
@@ -32,16 +33,18 @@ class ConnectFragment : Fragment() {
private lateinit var locationInfo: LocationInfo
private lateinit var parentActivity: MainActivity
- private lateinit var connectionProxy: ConnectionProxy
+ private lateinit var connectionProxy: SmartDeferred<ConnectionProxy>
private lateinit var keyStatusListener: KeyStatusListener
private lateinit var locationInfoCache: LocationInfoCache
private lateinit var relayListListener: RelayListListener
private lateinit var versionInfoCache: AppVersionInfoCache
private lateinit var updateKeyStatusJob: Job
- private lateinit var updateTunnelStateJob: Job
+ private var updateTunnelStateJob: Job? = null
+ private var tunnelStateSubscriptionJob: Long? = null
private var isTunnelInfoExpanded = false
+ private var tunnelStateListener: Int? = null
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -81,9 +84,9 @@ class ConnectFragment : Fragment() {
actionButton = ConnectActionButton(view)
actionButton.apply {
- onConnect = { connectionProxy.connect() }
- onCancel = { connectionProxy.disconnect() }
- onDisconnect = { connectionProxy.disconnect() }
+ onConnect = { connectionProxy.awaitThen { connect() } }
+ onCancel = { connectionProxy.awaitThen { disconnect() } }
+ onDisconnect = { connectionProxy.awaitThen { disconnect() } }
}
switchLocationButton = SwitchLocationButton(view, resources)
@@ -97,10 +100,10 @@ class ConnectFragment : Fragment() {
override fun onResume() {
super.onResume()
- notificationBanner.onResume()
-
locationInfo.isTunnelInfoExpanded = isTunnelInfoExpanded
+ notificationBanner.onResume()
+
keyStatusListener.onKeyStatusChange = { keyStatus ->
updateKeyStatusJob.cancel()
updateKeyStatusJob = updateKeyStatus(keyStatus)
@@ -114,10 +117,11 @@ class ConnectFragment : Fragment() {
switchLocationButton.location = selectedRelayItem
}
- updateTunnelStateJob = updateTunnelState(connectionProxy.uiState)
- connectionProxy.onUiStateChange = { uiState ->
- updateTunnelStateJob.cancel()
- updateTunnelStateJob = updateTunnelState(uiState)
+ tunnelStateSubscriptionJob = connectionProxy.awaitThen {
+ tunnelStateListener = onUiStateChange.subscribe { uiState ->
+ updateTunnelStateJob?.cancel()
+ updateTunnelStateJob = updateTunnelState(uiState, state)
+ }
}
}
@@ -126,14 +130,21 @@ class ConnectFragment : Fragment() {
locationInfoCache.onNewLocation = null
relayListListener.onRelayListChange = null
- connectionProxy.onUiStateChange = null
- updateTunnelStateJob.cancel()
-
+ tunnelStateSubscriptionJob?.let { jobId ->
+ connectionProxy.cancelJob(jobId)
+ }
- isTunnelInfoExpanded = locationInfo.isTunnelInfoExpanded
+ tunnelStateListener?.let { listener ->
+ connectionProxy.awaitThen {
+ onUiStateChange.unsubscribe(listener)
+ }
+ }
+ updateTunnelStateJob?.cancel()
notificationBanner.onPause()
+ isTunnelInfoExpanded = locationInfo.isTunnelInfoExpanded
+
super.onPause()
}
@@ -148,9 +159,9 @@ class ConnectFragment : Fragment() {
state.putBoolean(KEY_IS_TUNNEL_INFO_EXPANDED, isTunnelInfoExpanded)
}
- private fun updateTunnelState(uiState: TunnelState) = GlobalScope.launch(Dispatchers.Main) {
- val realState = connectionProxy.state
-
+ private fun updateTunnelState(uiState: TunnelState, realState: TunnelState) =
+ GlobalScope.launch(Dispatchers.Main)
+ {
locationInfoCache.state = realState
locationInfo.state = realState
headerBar.setState(realState)
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ForegroundNotificationManager.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ForegroundNotificationManager.kt
new file mode 100644
index 0000000000..5718e66036
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ForegroundNotificationManager.kt
@@ -0,0 +1,100 @@
+package net.mullvad.mullvadvpn
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.support.v4.app.NotificationCompat
+
+import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy
+import net.mullvad.mullvadvpn.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.model.TunnelState
+
+val FOREGROUND_NOTIFICATION_ID: Int = 1
+
+class ForegroundNotificationManager(val service: Service, val connectionProxy: ConnectionProxy) {
+ private var listenerId: Int? = null
+ private var reconnecting = false
+ private var showingReconnecting = false
+
+ private lateinit var notificationManager: NotificationManager
+
+ private var tunnelState: TunnelState = TunnelState.Disconnected()
+ set(value) {
+ field = value
+
+ reconnecting =
+ (value is TunnelState.Disconnecting
+ && value.actionAfterDisconnect is ActionAfterDisconnect.Reconnect)
+ || (value is TunnelState.Connecting && reconnecting)
+
+ updateNotification()
+ }
+
+ private val notificationText: Int
+ get() {
+ val state = tunnelState
+
+ return when (state) {
+ is TunnelState.Disconnected -> R.string.unsecured
+ is TunnelState.Connecting -> {
+ if (reconnecting) {
+ R.string.reconnecting
+ } else {
+ R.string.connecting
+ }
+ }
+ is TunnelState.Connected -> R.string.secured
+ is TunnelState.Disconnecting -> {
+ when (state.actionAfterDisconnect) {
+ is ActionAfterDisconnect.Reconnect -> R.string.reconnecting
+ else -> R.string.disconnecting
+ }
+ }
+ is TunnelState.Blocked -> R.string.blocking_all_connections
+ }
+ }
+
+ fun onCreate() {
+ notificationManager =
+ service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ listenerId = connectionProxy.onUiStateChange.subscribe { uiState ->
+ tunnelState = uiState
+ }
+
+ service.startForeground(FOREGROUND_NOTIFICATION_ID, buildNotification())
+ }
+
+ fun onDestroy() {
+ listenerId?.let { listener ->
+ connectionProxy.onUiStateChange.unsubscribe(listener)
+ }
+
+ service.stopForeground(FOREGROUND_NOTIFICATION_ID)
+ }
+
+ private fun updateNotification() {
+ if (!reconnecting || !showingReconnecting) {
+ notificationManager.notify(FOREGROUND_NOTIFICATION_ID, buildNotification())
+ }
+ }
+
+ private fun buildNotification(): Notification {
+ val intent = Intent(service, MainActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ .setAction(Intent.ACTION_MAIN)
+
+ val pendingIntent =
+ PendingIntent.getActivity(service, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+ return NotificationCompat.Builder(service)
+ .setSmallIcon(R.drawable.notification)
+ .setColor(service.getColor(R.color.colorPrimary))
+ .setContentTitle(service.getString(notificationText))
+ .setContentIntent(pendingIntent)
+ .build()
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt
index ffc11354ff..fff27be8b5 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MainActivity.kt
@@ -13,14 +13,12 @@ import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
-import android.net.VpnService
import android.os.Bundle
import android.os.IBinder
import android.support.v4.app.FragmentActivity
import net.mullvad.mullvadvpn.dataproxy.AccountCache
import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoCache
-import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy
import net.mullvad.mullvadvpn.dataproxy.KeyStatusListener
import net.mullvad.mullvadvpn.dataproxy.LocationInfoCache
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
@@ -30,9 +28,12 @@ import net.mullvad.mullvadvpn.model.RelaySettings
import net.mullvad.mullvadvpn.model.Settings
import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.relaylist.RelayList
+import net.mullvad.mullvadvpn.util.SmartDeferred
class MainActivity : FragmentActivity() {
- private var vpnPermission: CompletableDeferred<Boolean>? = null
+ companion object {
+ val KEY_SHOULD_CONNECT = "should_connect"
+ }
var daemon = CompletableDeferred<MullvadDaemon>()
private set
@@ -40,7 +41,7 @@ class MainActivity : FragmentActivity() {
private set
var appVersionInfoCache = AppVersionInfoCache(this)
- val connectionProxy = ConnectionProxy(this)
+ val connectionProxy = SmartDeferred(configureConnectionProxy())
val keyStatusListener = KeyStatusListener(daemon)
val problemReport = MullvadProblemReport()
var settingsListener = SettingsListener(this)
@@ -80,6 +81,10 @@ class MainActivity : FragmentActivity() {
}
appVersionInfoCache.onCreate()
+
+ if (intent.getBooleanExtra(KEY_SHOULD_CONNECT, false) ?: false) {
+ connectionProxy.awaitThen { connect() }
+ }
}
override fun onStart() {
@@ -92,11 +97,7 @@ class MainActivity : FragmentActivity() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
- if (resultCode == Activity.RESULT_OK) {
- vpnPermission?.complete(true)
- } else {
- vpnPermission?.complete(false)
- }
+ setVpnPermission(resultCode == Activity.RESULT_OK)
}
override fun onStop() {
@@ -110,6 +111,8 @@ class MainActivity : FragmentActivity() {
}
override fun onDestroy() {
+ connectionProxy.cancel()
+
accountCache.onDestroy()
appVersionInfoCache.onDestroy()
keyStatusListener.onDestroy()
@@ -136,19 +139,8 @@ class MainActivity : FragmentActivity() {
}
}
- fun requestVpnPermission(): Deferred<Boolean> {
- val intent = VpnService.prepare(this)
- val request = CompletableDeferred<Boolean>()
-
- vpnPermission = request
-
- if (intent != null) {
- startActivityForResult(intent, 0)
- } else {
- request.complete(true)
- }
-
- return request
+ fun requestVpnPermission(intent: Intent) {
+ startActivityForResult(intent, 0)
}
fun quit() {
@@ -163,6 +155,18 @@ class MainActivity : FragmentActivity() {
}
}
+ private fun configureConnectionProxy() = GlobalScope.async(Dispatchers.Default) {
+ service.await().connectionProxy.apply {
+ mainActivity = this@MainActivity
+ }
+ }
+
+ private fun setVpnPermission(allow: Boolean) = GlobalScope.launch(Dispatchers.Default) {
+ connectionProxy.awaitThen {
+ vpnPermission.complete(allow)
+ }
+ }
+
private fun fetchSettings() = GlobalScope.async(Dispatchers.Default) {
daemon.await().getSettings()
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt
index 2c51a9ecde..6ecfdb3535 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt
@@ -8,14 +8,13 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
-import android.app.Activity
-import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.Binder
import android.os.IBinder
import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoFetcher
+import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy
import net.mullvad.mullvadvpn.model.TunConfig
class MullvadVpnService : VpnService() {
@@ -25,9 +24,12 @@ class MullvadVpnService : VpnService() {
private lateinit var versionInfoFetcher: AppVersionInfoFetcher
val daemon = startDaemon()
+ val connectionProxy = ConnectionProxy(this, daemon)
+ val notificationManager = ForegroundNotificationManager(this, connectionProxy)
override fun onCreate() {
versionInfoFetcher = AppVersionInfoFetcher(daemon, this)
+ notificationManager.onCreate()
created.complete(Unit)
}
@@ -36,6 +38,8 @@ class MullvadVpnService : VpnService() {
}
override fun onDestroy() {
+ connectionProxy.onDestroy()
+ notificationManager.onDestroy()
versionInfoFetcher.stop()
daemon.cancel()
created.cancel()
@@ -70,6 +74,8 @@ class MullvadVpnService : VpnService() {
inner class LocalBinder : Binder() {
val daemon
get() = this@MullvadVpnService.daemon
+ val connectionProxy
+ get() = this@MullvadVpnService.connectionProxy
fun stop() {
if (daemon.isCompleted) {
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt
index 4a1849318c..f59387baa4 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/SelectLocationFragment.kt
@@ -26,10 +26,11 @@ 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.util.SmartDeferred
class SelectLocationFragment : Fragment() {
private lateinit var parentActivity: MainActivity
- private lateinit var connectionProxy: ConnectionProxy
+ private lateinit var connectionProxy: SmartDeferred<ConnectionProxy>
private lateinit var relayListListener: RelayListListener
private lateinit var relayListContainer: ViewSwitcher
@@ -129,7 +130,7 @@ class SelectLocationFragment : Fragment() {
val keyStatus = parentActivity.keyStatusListener.keyStatus
if (keyStatus == null || keyStatus is KeygenEvent.NewKey) {
- connectionProxy.connect()
+ connectionProxy.awaitThen { connect() }
}
}
}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt
index e8be9d7228..47e79fb74c 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/WireguardKeyFragment.kt
@@ -23,14 +23,17 @@ import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy
import net.mullvad.mullvadvpn.dataproxy.KeyStatusListener
import net.mullvad.mullvadvpn.model.KeygenEvent
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.util.SmartDeferred
class WireguardKeyFragment : Fragment() {
- private var TAG = "keyfragment";
- private var keyState: KeygenEvent? = null;
- private var currentJob: Job? = null;
- private var updateViewsJob: Job? = null;
+ private var keyState: KeygenEvent? = null
+ private var currentJob: Job? = null
+ private var updateViewsJob: Job? = null
+ private var tunnelStateListener: Int? = null
+ private var tunnelStateSubscriptionJob: Long? = null
+ private var tunnelState: TunnelState = TunnelState.Disconnected()
private lateinit var parentActivity: MainActivity
- private lateinit var connectionProxy: ConnectionProxy
+ private lateinit var connectionProxy: SmartDeferred<ConnectionProxy>
private lateinit var keyStatusListener: KeyStatusListener
private var generatingKey = false
private var validatingKey = false
@@ -69,16 +72,6 @@ class WireguardKeyFragment : Fragment() {
updateViews()
- connectionProxy.onUiStateChange = { _ ->
- updateViewsJob?.cancel()
- updateViewsJob = updateViewJob()
- }
-
- keyStatusListener.onKeyStatusChange = { _ ->
- updateViewsJob?.cancel()
- updateViewsJob = updateViewJob()
- }
-
return view
}
@@ -145,7 +138,7 @@ class WireguardKeyFragment : Fragment() {
private fun setGenerateButton() {
if (generatingKey) {
showActionSpinner()
- return;
+ return
}
actionSpinner.visibility = View.GONE
actionButton.visibility = View.VISIBLE
@@ -158,7 +151,7 @@ class WireguardKeyFragment : Fragment() {
private fun setValidateButton() {
if (validatingKey) {
showActionSpinner()
- return;
+ return
}
actionSpinner.visibility = View.GONE
actionButton.visibility = View.VISIBLE
@@ -174,7 +167,7 @@ class WireguardKeyFragment : Fragment() {
}
private fun drawNoConnectionState() {
- when (connectionProxy.state) {
+ when (tunnelState) {
is TunnelState.Connecting, is TunnelState.Disconnecting -> {
statusMessage.setText(R.string.wireguard_key_connectivity)
statusMessage.visibility = View.VISIBLE
@@ -186,24 +179,24 @@ class WireguardKeyFragment : Fragment() {
private fun onGenerateKeyPress() {
currentJob?.cancel()
- generatingKey = true;
- validatingKey = false;
+ generatingKey = true
+ validatingKey = false
updateViews()
currentJob = GlobalScope.launch(Dispatchers.Main) {
keyStatusListener.generateKey().join()
- generatingKey = false;
+ generatingKey = false
updateViews()
}
}
private fun onValidateKeyPress() {
currentJob?.cancel()
- validatingKey = true;
- generatingKey = false;
+ validatingKey = true
+ generatingKey = false
updateViews()
currentJob = GlobalScope.launch(Dispatchers.Main) {
keyStatusListener.verifyKey().join()
- validatingKey = false;
+ validatingKey = false
when (val state = keyStatusListener.keyStatus) {
is KeygenEvent.NewKey -> {
if (state.verified == null) {
@@ -216,20 +209,33 @@ class WireguardKeyFragment : Fragment() {
}
override fun onPause() {
- connectionProxy.onUiStateChange = null
+ tunnelStateSubscriptionJob?.let { jobId ->
+ connectionProxy.cancelJob(jobId)
+ }
+
+ tunnelStateListener?.let { listener ->
+ connectionProxy.awaitThen {
+ onUiStateChange.unsubscribe(listener)
+ }
+ }
+
keyStatusListener.onKeyStatusChange = null
currentJob?.cancel()
updateViewsJob?.cancel()
- validatingKey = false;
- generatingKey = false;
+ validatingKey = false
+ generatingKey = false
super.onPause()
}
override fun onResume() {
super.onResume()
- connectionProxy.onUiStateChange = { _ ->
- updateViewsJob?.cancel()
- updateViewsJob = updateViewJob()
+
+ tunnelStateSubscriptionJob = connectionProxy.awaitThen {
+ tunnelStateListener = onUiStateChange.subscribe { uiState ->
+ tunnelState = uiState
+ updateViewsJob?.cancel()
+ updateViewsJob = updateViewJob()
+ }
}
keyStatusListener.onKeyStatusChange = { _ ->
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/ConnectionProxy.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/ConnectionProxy.kt
index 26abe8a106..2c46e34292 100644
--- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/ConnectionProxy.kt
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/ConnectionProxy.kt
@@ -1,16 +1,24 @@
package net.mullvad.mullvadvpn.dataproxy
+import android.content.Context
+import android.content.Intent
+import android.net.VpnService
+
import kotlinx.coroutines.launch
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import net.mullvad.mullvadvpn.MainActivity
+import net.mullvad.mullvadvpn.MullvadDaemon
import net.mullvad.mullvadvpn.model.ActionAfterDisconnect
import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.util.EventNotifier
-class ConnectionProxy(val parentActivity: MainActivity) {
- val daemon = parentActivity.daemon
+class ConnectionProxy(val context: Context, val daemon: Deferred<MullvadDaemon>) {
+ var mainActivity: MainActivity? = null
private var activeAction: Job? = null
@@ -31,20 +39,17 @@ class ConnectionProxy(val parentActivity: MainActivity) {
var uiState: TunnelState = TunnelState.Disconnected()
private set(value) {
field = value
- onUiStateChange?.invoke(value)
+ onUiStateChange.notify(value)
}
- var onUiStateChange: ((TunnelState) -> Unit)? = null
- set(value) {
- field = value
- value?.invoke(uiState)
- }
+ var onUiStateChange = EventNotifier(uiState)
+ var vpnPermission = CompletableDeferred<Boolean>()
fun connect() {
if (anticipateConnectingState()) {
cancelActiveAction()
- val vpnPermission = parentActivity.requestVpnPermission()
+ requestVpnPermission()
activeAction = GlobalScope.launch(Dispatchers.Default) {
if (vpnPermission.await()) {
@@ -68,6 +73,7 @@ class ConnectionProxy(val parentActivity: MainActivity) {
}
fun onDestroy() {
+ onUiStateChange.unsubscribeAll()
attachListenerJob.cancel()
detachListener()
fetchInitialStateJob.cancel()
@@ -100,6 +106,27 @@ class ConnectionProxy(val parentActivity: MainActivity) {
}
}
+ private fun requestVpnPermission() {
+ val intent = VpnService.prepare(context)
+
+ vpnPermission = CompletableDeferred()
+
+ if (intent == null) {
+ vpnPermission.complete(true)
+ } else {
+ val activity = mainActivity
+
+ if (activity != null) {
+ activity.requestVpnPermission(intent)
+ } else {
+ val activityIntent = Intent(context, MainActivity::class.java)
+ .putExtra(MainActivity.KEY_SHOULD_CONNECT, true)
+
+ context.startActivity(activityIntent)
+ }
+ }
+ }
+
private fun fetchInitialState() = GlobalScope.launch(Dispatchers.Default) {
val initialState = daemon.await().getState()
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/EventNotifier.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/EventNotifier.kt
new file mode 100644
index 0000000000..ca45ca96e6
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/EventNotifier.kt
@@ -0,0 +1,42 @@
+package net.mullvad.mullvadvpn.util
+
+class EventNotifier<T>(private val initialValue: T) {
+ private val listeners = HashMap<Int, (T) -> Unit>()
+
+ private var idCounter = 0
+ private var latestEvent = initialValue
+
+ fun notify(event: T) {
+ synchronized(this) {
+ for (listener in listeners.values) {
+ listener(event)
+ }
+
+ latestEvent = event
+ }
+ }
+
+ fun subscribe(listener: (T) -> Unit): Int {
+ synchronized(this) {
+ val id = idCounter
+
+ idCounter += 1
+ listeners.put(id, listener)
+ listener(latestEvent)
+
+ return id
+ }
+ }
+
+ fun unsubscribe(id: Int) {
+ synchronized(this) {
+ listeners.remove(id)
+ }
+ }
+
+ fun unsubscribeAll() {
+ synchronized(this) {
+ listeners.clear()
+ }
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt
new file mode 100644
index 0000000000..4934669d58
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt
@@ -0,0 +1,46 @@
+package net.mullvad.mullvadvpn.util
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+
+class JobTracker {
+ private val jobs = HashMap<Long, Job>()
+
+ private var jobIdCounter = 0L
+
+ fun newJob(job: Job): Long {
+ synchronized(jobs) {
+ val jobId = jobIdCounter
+
+ jobIdCounter += 1
+
+ jobs.put(jobId, GlobalScope.launch(Dispatchers.Default) {
+ job.join()
+
+ synchronized(jobs) {
+ jobs.remove(jobId)
+ }
+ })
+
+ return jobId
+ }
+ }
+
+ fun cancelJob(jobId: Long) {
+ synchronized(jobs) {
+ jobs.remove(jobId)?.cancel()
+ }
+ }
+
+ fun cancelAllJobs() {
+ synchronized(jobs) {
+ for (job in jobs.values) {
+ job.cancel()
+ }
+
+ jobs.clear()
+ }
+ }
+}
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt
new file mode 100644
index 0000000000..a7732c08ef
--- /dev/null
+++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt
@@ -0,0 +1,32 @@
+package net.mullvad.mullvadvpn.util
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+
+class SmartDeferred<T>(private val deferred: Deferred<T>) {
+ private val jobTracker = JobTracker()
+
+ private var active = true
+
+ fun awaitThen(action: T.() -> Unit): Long? {
+ if (active) {
+ return jobTracker.newJob(GlobalScope.launch(Dispatchers.Default) {
+ deferred.await().action()
+ })
+ } else {
+ return null
+ }
+ }
+
+ fun cancelJob(jobId: Long) {
+ jobTracker.cancelJob(jobId)
+ }
+
+ fun cancel() {
+ active = false
+ jobTracker.cancelAllJobs()
+ }
+}
diff --git a/android/src/main/res/drawable-hdpi/notification.png b/android/src/main/res/drawable-hdpi/notification.png
new file mode 100644
index 0000000000..a6aa370201
--- /dev/null
+++ b/android/src/main/res/drawable-hdpi/notification.png
Binary files differ
diff --git a/android/src/main/res/drawable-mdpi/notification.png b/android/src/main/res/drawable-mdpi/notification.png
new file mode 100644
index 0000000000..d1a7dc4d5f
--- /dev/null
+++ b/android/src/main/res/drawable-mdpi/notification.png
Binary files differ
diff --git a/android/src/main/res/drawable-xhdpi/notification.png b/android/src/main/res/drawable-xhdpi/notification.png
new file mode 100644
index 0000000000..7ce258b490
--- /dev/null
+++ b/android/src/main/res/drawable-xhdpi/notification.png
Binary files differ
diff --git a/android/src/main/res/drawable-xxhdpi/notification.png b/android/src/main/res/drawable-xxhdpi/notification.png
new file mode 100644
index 0000000000..edebf879d3
--- /dev/null
+++ b/android/src/main/res/drawable-xxhdpi/notification.png
Binary files differ
diff --git a/android/src/main/res/drawable-xxxhdpi/notification.png b/android/src/main/res/drawable-xxxhdpi/notification.png
new file mode 100644
index 0000000000..b2b88998cb
--- /dev/null
+++ b/android/src/main/res/drawable-xxxhdpi/notification.png
Binary files differ
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index 50e4d43c79..986e0fc623 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -1,6 +1,13 @@
<resources>
<string name="app_name">Mullvad VPN</string>
+ <string name="connecting">Connecting</string>
+ <string name="reconnecting">Reconnecting</string>
+ <string name="disconnecting">Disconnecting</string>
+ <string name="secured">Secured</string>
+ <string name="unsecured">Unsecured</string>
+ <string name="blocking_all_connections">Blocking all connections</string>
+
<string name="connecting_to_daemon">Connecting to Mullvad system service...</string>
<string name="login_title">Login</string>