From c359d78bcd45cb506bac51a616ef62af0845df85 Mon Sep 17 00:00:00 2001
From: pacien
Date: Fri, 16 Feb 2018 18:23:01 +0100
Subject: Refactor activities and service, locking app at daemon startup and
shutdown
---
app/src/main/AndroidManifest.xml | 15 +-
.../pacien/tincapp/activities/LaunchActivity.kt | 98 -------------
.../org/pacien/tincapp/activities/StartActivity.kt | 152 ++++++++++++++++-----
.../pacien/tincapp/activities/StatusActivity.kt | 99 +++++++++-----
.../main/java/org/pacien/tincapp/commands/Tinc.kt | 4 +
.../main/java/org/pacien/tincapp/intent/Actions.kt | 19 +++
.../tincapp/intent/SimpleBroadcastReceiver.kt | 19 +++
.../org/pacien/tincapp/intent/action/Actions.kt | 14 --
.../org/pacien/tincapp/service/TincVpnService.kt | 113 ++++++++-------
.../java/org/pacien/tincapp/utils/TincKeyring.kt | 26 ++++
app/src/main/res/values/strings.xml | 4 +
11 files changed, 323 insertions(+), 240 deletions(-)
delete mode 100644 app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt
create mode 100644 app/src/main/java/org/pacien/tincapp/intent/Actions.kt
create mode 100644 app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt
delete mode 100644 app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt
create mode 100644 app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f03a640..6826920 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,6 +24,11 @@
+
+
+
+
+
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt
deleted file mode 100644
index 0179040..0000000
--- a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package org.pacien.tincapp.activities
-
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.content.Intent
-import android.net.Uri
-import android.net.VpnService
-import android.os.Bundle
-import android.support.v7.app.AlertDialog
-import android.support.v7.app.AppCompatActivity
-import kotlinx.android.synthetic.main.dialog_decrypt_keys.view.*
-import org.pacien.tincapp.R
-import org.pacien.tincapp.commands.TincApp
-import org.pacien.tincapp.context.App
-import org.pacien.tincapp.intent.action.ACTION_CONNECT
-import org.pacien.tincapp.intent.action.ACTION_DISCONNECT
-import org.pacien.tincapp.intent.action.TINC_SCHEME
-import org.pacien.tincapp.service.TincVpnService
-import org.pacien.tincapp.utils.PemUtils
-import java.io.FileNotFoundException
-
-/**
- * @author pacien
- */
-class LaunchActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- when (intent.action) {
- ACTION_CONNECT -> requestPerm()
- ACTION_DISCONNECT -> disconnect()
- }
- }
-
- override fun onActivityResult(request: Int, result: Int, data: Intent?) {
- if (request == PERMISSION_REQUEST_CODE && result == Activity.RESULT_OK) askPassphrase()
- }
-
- private fun requestPerm() = VpnService.prepare(this).let {
- if (it != null)
- startActivityForResult(it, PERMISSION_REQUEST_CODE)
- else
- onActivityResult(PERMISSION_REQUEST_CODE, Activity.RESULT_OK, null)
- }
-
- @SuppressLint("InflateParams")
- private fun askPassphrase() {
- val netName = intent.data.schemeSpecificPart
-
- if (needPassphrase(netName) && intent.data.fragment == null) {
- val dialog = layoutInflater.inflate(R.layout.dialog_decrypt_keys, null, false)
- AlertDialog.Builder(this)
- .setTitle(R.string.title_unlock_private_keys).setView(dialog)
- .setPositiveButton(R.string.action_unlock) { _, _ -> connect(netName, dialog.passphrase.text.toString()) }
- .setNegativeButton(R.string.action_cancel, { _, _ -> finish() })
- .show()
- } else {
- connect(netName, intent.data.fragment)
- }
- }
-
- private fun needPassphrase(netName: String) = try {
- TincApp.listPrivateKeys(netName).filter { it.exists() }.any { PemUtils.isEncrypted(PemUtils.read(it)) }
- } catch (e: FileNotFoundException) {
- false
- }
-
- private fun connect(netName: String, passphrase: String? = null) {
- TincVpnService.startVpn(netName, passphrase)
- finish()
- }
-
- private fun disconnect() {
- TincVpnService.stopVpn()
- finish()
- }
-
- companion object {
-
- private val PERMISSION_REQUEST_CODE = 0
-
- fun connect(netName: String, passphrase: String? = null) {
- App.getContext().startActivity(Intent(App.getContext(), LaunchActivity::class.java)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .setAction(ACTION_CONNECT)
- .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build()))
- }
-
- fun disconnect() {
- App.getContext().startActivity(Intent(App.getContext(), LaunchActivity::class.java)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .setAction(ACTION_DISCONNECT))
- }
-
- }
-
-}
diff --git a/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt
index 719bbc1..9fa5e44 100644
--- a/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt
+++ b/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt
@@ -1,8 +1,13 @@
package org.pacien.tincapp.activities
+import android.app.Activity
+import android.app.ProgressDialog
import android.content.Intent
+import android.content.IntentFilter
+import android.net.VpnService
import android.os.Bundle
import android.support.v4.widget.SwipeRefreshLayout
+import android.support.v7.app.AlertDialog
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -10,29 +15,117 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import kotlinx.android.synthetic.main.base.*
+import kotlinx.android.synthetic.main.dialog_decrypt_keys.view.*
import kotlinx.android.synthetic.main.fragment_list_view.*
import kotlinx.android.synthetic.main.fragment_network_list_header.*
import org.pacien.tincapp.R
import org.pacien.tincapp.context.AppPaths
import org.pacien.tincapp.extensions.Android.setElements
+import org.pacien.tincapp.intent.Actions
+import org.pacien.tincapp.intent.SimpleBroadcastReceiver
import org.pacien.tincapp.service.TincVpnService
+import org.pacien.tincapp.utils.TincKeyring
/**
* @author pacien
*/
-class StartActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener {
+class StartActivity : BaseActivity() {
+ companion object {
+ private const val PERMISSION_REQUEST = 0
+ }
+
+ private val networkList = object : AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener {
+ private var networkListAdapter: ArrayAdapter? = null
+
+ fun init() {
+ networkListAdapter = ArrayAdapter(this@StartActivity, R.layout.fragment_list_item)
+ layoutInflater.inflate(R.layout.fragment_list_view, main_content)
+ list_wrapper.setOnRefreshListener(this)
+ list.addHeaderView(layoutInflater.inflate(R.layout.fragment_network_list_header, list, false), null, false)
+ list.addFooterView(View(this@StartActivity), null, false)
+ list.adapter = networkListAdapter
+ list.onItemClickListener = this
+ }
+
+ fun destroy() {
+ networkListAdapter = null
+ }
+
+ override fun onRefresh() {
+ val networks = AppPaths.confDir()?.list()?.toList() ?: emptyList()
+ runOnUiThread {
+ networkListAdapter?.setElements(networks)
+ setPlaceholderVisibility()
+ list_wrapper.isRefreshing = false
+ }
+ }
+
+ override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ connectionStarter.tryStart(netName = (view as TextView).text.toString(), displayStatus = true)
+ }
+
+ private fun setPlaceholderVisibility() = if (networkListAdapter?.isEmpty != false) {
+ network_list_placeholder.text = getListPlaceholderText()
+ network_list_placeholder.visibility = View.VISIBLE
+ } else {
+ network_list_placeholder.visibility = View.GONE
+ }
+
+ private fun getListPlaceholderText() = if (!AppPaths.storageAvailable()) {
+ getText(R.string.message_storage_unavailable)
+ } else {
+ getText(R.string.message_no_network_configuration_found)
+ }
+ }
+
+ private val connectionStarter = object {
+ private var netName: String? = null
+ private var passphrase: String? = null
+ private var displayStatus = false
+
+ fun displayStatus() = displayStatus
- private var networkListAdapter: ArrayAdapter? = null
+ fun tryStart(netName: String? = null, passphrase: String? = null, displayStatus: Boolean? = null) {
+ if (netName != null) this.netName = netName
+ if (passphrase != null) this.passphrase = passphrase
+ if (displayStatus != null) this.displayStatus = displayStatus
+
+ val permissionRequestIntent = VpnService.prepare(this@StartActivity)
+ if (permissionRequestIntent != null)
+ return startActivityForResult(permissionRequestIntent, PERMISSION_REQUEST)
+
+ if (TincKeyring.needsPassphrase(this.netName!!) && this.passphrase == null)
+ return askForPassphrase()
+
+ startVpn(this.netName!!, this.passphrase)
+ }
+
+ private fun askForPassphrase() {
+ layoutInflater.inflate(R.layout.dialog_decrypt_keys, main_content, false).let { dialog ->
+ AlertDialog.Builder(this@StartActivity)
+ .setTitle(R.string.title_unlock_private_keys).setView(dialog)
+ .setPositiveButton(R.string.action_unlock) { _, _ -> tryStart(passphrase = dialog.passphrase.text.toString()) }
+ .setNegativeButton(R.string.action_cancel, { _, _ -> Unit })
+ .show()
+ }
+ }
+
+ private fun startVpn(netName: String, passphrase: String? = null) {
+ connectDialog = showProgressDialog(R.string.message_starting_vpn)
+ TincVpnService.connect(netName, passphrase)
+ }
+ }
+
+ private val startupBroadcastReceiver = SimpleBroadcastReceiver(IntentFilter(Actions.EVENT_CONNECTED), this::onVpnStart)
+
+ private var connectDialog: ProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- networkListAdapter = ArrayAdapter(this, R.layout.fragment_list_item)
- layoutInflater.inflate(R.layout.fragment_list_view, main_content)
- list_wrapper.setOnRefreshListener(this)
- list.addHeaderView(layoutInflater.inflate(R.layout.fragment_network_list_header, list, false), null, false)
- list.addFooterView(View(this), null, false)
- list.adapter = networkListAdapter
- list.onItemClickListener = this
+ networkList.init()
+
+ if (intent.action == Actions.ACTION_CONNECT && intent.data?.schemeSpecificPart != null)
+ connectionStarter.tryStart(intent.data.schemeSpecificPart, intent.data.fragment, false)
}
override fun onCreateOptionsMenu(m: Menu): Boolean {
@@ -41,48 +134,41 @@ class StartActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefr
}
override fun onDestroy() {
- networkListAdapter = null
+ networkList.destroy()
+ connectDialog?.dismiss()
super.onDestroy()
}
override fun onStart() {
super.onStart()
- onRefresh()
+ networkList.onRefresh()
}
override fun onResume() {
super.onResume()
if (TincVpnService.isConnected()) openStatusActivity()
+ startupBroadcastReceiver.register()
}
- override fun onRefresh() {
- val networks = AppPaths.confDir()?.list()?.toList() ?: emptyList()
- runOnUiThread {
- networkListAdapter?.setElements(networks)
- setPlaceholderVisibility()
- list_wrapper.isRefreshing = false
- }
+ override fun onPause() {
+ startupBroadcastReceiver.unregister()
+ super.onPause()
}
- override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) =
- LaunchActivity.connect((view as TextView).text.toString())
+ override fun onActivityResult(request: Int, result: Int, data: Intent?): Unit = when (request) {
+ PERMISSION_REQUEST -> if (result == Activity.RESULT_OK) connectionStarter.tryStart() else Unit
+ else -> throw IllegalArgumentException("Result for unknown request received.")
+ }
fun openConfigureActivity(@Suppress("UNUSED_PARAMETER") i: MenuItem) =
startActivity(Intent(this, ConfigureActivity::class.java))
- fun openStatusActivity() =
- startActivity(Intent(this, StatusActivity::class.java)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK))
-
- private fun setPlaceholderVisibility() = if (networkListAdapter?.isEmpty != false) {
- network_list_placeholder.text = getListPlaceholderText()
- network_list_placeholder.visibility = View.VISIBLE
- } else {
- network_list_placeholder.visibility = View.GONE
+ private fun onVpnStart() {
+ connectDialog?.dismiss()
+ if (connectionStarter.displayStatus()) openStatusActivity()
+ finish()
}
- private fun getListPlaceholderText() =
- if (!AppPaths.storageAvailable()) getText(R.string.message_storage_unavailable)
- else getText(R.string.message_no_network_configuration_found)
-
+ private fun openStatusActivity() =
+ startActivity(Intent(this, StatusActivity::class.java))
}
diff --git a/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt
index 4b5384c..dc45947 100644
--- a/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt
+++ b/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt
@@ -1,6 +1,8 @@
package org.pacien.tincapp.activities
+import android.app.ProgressDialog
import android.content.Intent
+import android.content.IntentFilter
import android.os.Bundle
import android.support.v4.widget.SwipeRefreshLayout
import android.support.v7.app.AlertDialog
@@ -20,6 +22,8 @@ import org.pacien.tincapp.commands.Tinc
import org.pacien.tincapp.data.VpnInterfaceConfiguration
import org.pacien.tincapp.extensions.Android.setElements
import org.pacien.tincapp.extensions.Android.setText
+import org.pacien.tincapp.intent.Actions
+import org.pacien.tincapp.intent.SimpleBroadcastReceiver
import org.pacien.tincapp.service.TincVpnService
import java.util.*
import kotlin.concurrent.timerTask
@@ -28,16 +32,15 @@ import kotlin.concurrent.timerTask
* @author pacien
*/
class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener {
-
+ private val shutdownBroadcastReceiver = SimpleBroadcastReceiver(IntentFilter(Actions.EVENT_DISCONNECTED), this::onVpnShutdown)
+ private var shutdownDialog: ProgressDialog? = null
private var nodeListAdapter: ArrayAdapter? = null
private var refreshTimer: Timer? = null
- private var updateView: Boolean = false
+ private var listNetworksAfterExit = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
nodeListAdapter = ArrayAdapter(this, R.layout.fragment_list_item)
- refreshTimer = Timer(true)
layoutInflater.inflate(R.layout.fragment_list_view, main_content)
list_wrapper.setOnRefreshListener(this)
@@ -45,6 +48,13 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef
list.addFooterView(View(this), null, false)
list.onItemClickListener = this
list.adapter = nodeListAdapter
+
+ if (intent.action == Actions.ACTION_DISCONNECT) {
+ listNetworksAfterExit = false
+ stopVpn()
+ } else {
+ listNetworksAfterExit = true
+ }
}
override fun onCreateOptionsMenu(m: Menu): Boolean {
@@ -54,38 +64,35 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef
override fun onDestroy() {
super.onDestroy()
- refreshTimer?.cancel()
nodeListAdapter = null
refreshTimer = null
}
override fun onStart() {
super.onStart()
+ refreshTimer = Timer(true)
+ refreshTimer?.schedule(timerTask { updateView() }, NOW, REFRESH_RATE)
writeNetworkInfo(TincVpnService.getCurrentInterfaceCfg() ?: VpnInterfaceConfiguration())
- updateView = true
- onRefresh()
- updateNodeList()
}
override fun onStop() {
+ refreshTimer?.cancel()
super.onStop()
- updateView = false
}
override fun onResume() {
super.onResume()
- if (!TincVpnService.isConnected()) openStartActivity()
+ shutdownBroadcastReceiver.register()
+ updateView()
+ }
+
+ override fun onPause() {
+ shutdownBroadcastReceiver.unregister()
+ super.onPause()
}
override fun onRefresh() {
- getNodeNames().thenAccept {
- runOnUiThread {
- nodeListAdapter?.setElements(it)
- node_list_placeholder.visibility = if (nodeListAdapter?.isEmpty != false) View.VISIBLE else View.GONE
- list_wrapper.isRefreshing = false
- if (!TincVpnService.isConnected()) openStartActivity()
- }
- }
+ refreshTimer?.schedule(timerTask { updateView() }, NOW)
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
@@ -97,13 +104,26 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef
AlertDialog.Builder(this)
.setTitle(R.string.title_node_info)
.setView(dialogTextView)
- .setPositiveButton(R.string.action_close) { _, _ -> /* nop */ }
+ .setPositiveButton(R.string.action_close) { _, _ -> Unit }
.show()
}
}
}
- fun writeNetworkInfo(cfg: VpnInterfaceConfiguration) {
+ private fun onVpnShutdown() {
+ shutdownDialog?.dismiss()
+ if (listNetworksAfterExit) openStartActivity()
+ finish()
+ }
+
+ fun stopVpn(@Suppress("UNUSED_PARAMETER") i: MenuItem? = null) {
+ refreshTimer?.cancel()
+ list_wrapper.isRefreshing = false
+ shutdownDialog = showProgressDialog(R.string.message_disconnecting_vpn)
+ TincVpnService.disconnect()
+ }
+
+ private fun writeNetworkInfo(cfg: VpnInterfaceConfiguration) {
text_network_name.text = TincVpnService.getCurrentNetName() ?: getString(R.string.value_none)
text_network_ip_addresses.setText(cfg.addresses.map { it.toSlashSeparated() })
text_network_routes.setText(cfg.routes.map { it.toSlashSeparated() })
@@ -116,28 +136,35 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef
text_network_disallowed_applications.setText(cfg.disallowedApplications)
}
- fun updateNodeList() {
- refreshTimer?.schedule(timerTask {
- onRefresh()
- if (updateView) updateNodeList()
- }, REFRESH_RATE)
+ private fun writeNodeList(nodeList: List) = runOnUiThread {
+ nodeListAdapter?.setElements(nodeList)
+ node_list_placeholder.visibility = if (nodeListAdapter?.isEmpty != false) View.VISIBLE else View.GONE
+ list_wrapper.isRefreshing = false
}
- fun stopVpn(@Suppress("UNUSED_PARAMETER") i: MenuItem) {
- TincVpnService.stopVpn()
- openStartActivity()
- finish()
+ private fun updateNodeList() {
+ getNodeNames().whenComplete { nodeList, _ -> runOnUiThread { writeNodeList(nodeList) } }
}
- fun openStartActivity() = startActivity(Intent(this, StartActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
+ private fun updateView() = when {
+ TincVpnService.isConnected() -> updateNodeList()
+ else -> openStartActivity()
+ }
- companion object {
- private val REFRESH_RATE = 5000L
+ private fun openStartActivity() {
+ startActivity(Intent(this, StartActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
+ finish()
+ }
- fun getNodeNames(): CompletableFuture> = when (TincVpnService.isConnected()) {
- true -> Tinc.dumpNodes(TincVpnService.getCurrentNetName()!!).thenApply> { it.map { it.substringBefore(' ') } }
- false -> CompletableFuture.supplyAsync> { emptyList() }
+ companion object {
+ private const val REFRESH_RATE = 5000L
+ private const val NOW = 0L
+
+ fun getNodeNames(): CompletableFuture> = when {
+ TincVpnService.isConnected() ->
+ Tinc.dumpNodes(TincVpnService.getCurrentNetName()!!).thenApply> { it.map { it.substringBefore(' ') } }
+ else ->
+ CompletableFuture.supplyAsync> { emptyList() }
}
}
-
}
diff --git a/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt b/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt
index 0b1240a..e0cdb12 100644
--- a/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt
+++ b/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt
@@ -17,6 +17,10 @@ object Tinc {
Executor.call(newCommand(netName).withArguments("stop"))
.thenApply { }
+ fun pid(netName: String): CompletableFuture =
+ Executor.call(newCommand(netName).withArguments("pid"))
+ .thenApply { Integer.parseInt(it.first()) }
+
fun dumpNodes(netName: String, reachable: Boolean = false): CompletableFuture> =
Executor.call(
if (reachable) newCommand(netName).withArguments("dump", "reachable", "nodes")
diff --git a/app/src/main/java/org/pacien/tincapp/intent/Actions.kt b/app/src/main/java/org/pacien/tincapp/intent/Actions.kt
new file mode 100644
index 0000000..4650952
--- /dev/null
+++ b/app/src/main/java/org/pacien/tincapp/intent/Actions.kt
@@ -0,0 +1,19 @@
+package org.pacien.tincapp.intent
+
+import android.net.Uri
+import org.pacien.tincapp.BuildConfig
+
+/**
+ * @author pacien
+ */
+object Actions {
+ const val PREFIX = "${BuildConfig.APPLICATION_ID}.intent.action"
+ const val ACTION_CONNECT = "$PREFIX.CONNECT"
+ const val ACTION_DISCONNECT = "$PREFIX.DISCONNECT"
+ const val EVENT_CONNECTED = "$PREFIX.CONNECTED"
+ const val EVENT_DISCONNECTED = "$PREFIX.DISCONNECTED"
+ const val TINC_SCHEME = "tinc"
+
+ fun buildNetworkUri(netName: String, passphrase: String? = null): Uri =
+ Uri.Builder().scheme(Actions.TINC_SCHEME).opaquePart(netName).fragment(passphrase).build()
+}
diff --git a/app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt b/app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt
new file mode 100644
index 0000000..fb77174
--- /dev/null
+++ b/app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt
@@ -0,0 +1,19 @@
+package org.pacien.tincapp.intent
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.support.v4.content.LocalBroadcastManager
+import org.pacien.tincapp.context.App
+
+/**
+ * @author pacien
+ */
+class SimpleBroadcastReceiver(private val intentFilter: IntentFilter, private val eventHandler: () -> Unit) : BroadcastReceiver() {
+ private val broadcastManager = LocalBroadcastManager.getInstance(App.getContext())
+
+ fun register() = broadcastManager.registerReceiver(this, intentFilter)
+ fun unregister() = broadcastManager.unregisterReceiver(this)
+ override fun onReceive(context: Context?, intent: Intent?) = eventHandler()
+}
diff --git a/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt b/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt
deleted file mode 100644
index ece9b68..0000000
--- a/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.pacien.tincapp.intent.action
-
-import org.pacien.tincapp.BuildConfig
-
-/**
- * @author pacien
- */
-
-private val PREFIX = "${BuildConfig.APPLICATION_ID}.intent.action"
-
-val ACTION_CONNECT = "$PREFIX.CONNECT"
-val ACTION_DISCONNECT = "$PREFIX.DISCONNECT"
-
-val TINC_SCHEME = "tinc"
diff --git a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
index ec0512a..ce41b89 100644
--- a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
+++ b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
@@ -2,9 +2,9 @@ package org.pacien.tincapp.service
import android.app.Service
import android.content.Intent
-import android.net.Uri
import android.net.VpnService
import android.os.ParcelFileDescriptor
+import android.support.v4.content.LocalBroadcastManager
import android.util.Log
import java8.util.concurrent.CompletableFuture
import org.apache.commons.configuration2.ex.ConversionException
@@ -19,32 +19,41 @@ import org.pacien.tincapp.data.TincConfiguration
import org.pacien.tincapp.data.VpnInterfaceConfiguration
import org.pacien.tincapp.extensions.Java.applyIgnoringException
import org.pacien.tincapp.extensions.VpnServiceBuilder.applyCfg
-import org.pacien.tincapp.intent.action.TINC_SCHEME
-import org.pacien.tincapp.utils.PemUtils
-import java.io.File
+import org.pacien.tincapp.intent.Actions
+import org.pacien.tincapp.utils.TincKeyring
import java.io.FileNotFoundException
-import java.io.IOException
/**
* @author pacien
*/
class TincVpnService : VpnService() {
-
override fun onDestroy() {
stopVpn()
super.onDestroy()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
- if (isConnected()) stopVpn()
- startVpn(intent.data.schemeSpecificPart, intent.data.fragment)
- return Service.START_REDELIVER_INTENT
+ Log.i(TAG, intent.action)
+
+ when {
+ intent.action == Actions.ACTION_CONNECT && intent.scheme == Actions.TINC_SCHEME ->
+ startVpn(intent.data.schemeSpecificPart, intent.data.fragment)
+ intent.action == Actions.ACTION_DISCONNECT ->
+ stopVpn()
+ else ->
+ throw IllegalArgumentException("Invalid intent action received.")
+ }
+
+ return Service.START_NOT_STICKY
}
- private fun startVpn(netName: String, passphrase: String? = null) {
+ private fun startVpn(netName: String, passphrase: String? = null): Unit = synchronized(this) {
if (netName.isBlank())
return reportError(resources.getString(R.string.message_no_network_name_provided), docTopic = "intent-api")
+ if (TincKeyring.needsPassphrase(netName) && passphrase == null)
+ return reportError(resources.getString(R.string.message_passphrase_required))
+
if (!AppPaths.storageAvailable())
return reportError(resources.getString(R.string.message_storage_unavailable))
@@ -52,6 +61,7 @@ class TincVpnService : VpnService() {
return reportError(resources.getString(R.string.message_no_configuration_for_network_format, netName), docTopic = "configuration")
Log.i(TAG, "Starting tinc daemon for network \"$netName\".")
+ if (isConnected()) stopVpn()
val interfaceCfg = try {
VpnInterfaceConfiguration.fromIfaceConfiguration(AppPaths.existing(AppPaths.netConfFile(netName)))
@@ -65,37 +75,41 @@ class TincVpnService : VpnService() {
Builder().setSession(netName)
.applyCfg(interfaceCfg)
.also { applyIgnoringException(it::addDisallowedApplication, BuildConfig.APPLICATION_ID) }
- .establish()
+ .establish()!!
} catch (e: IllegalArgumentException) {
return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface")
}
val privateKeys = try {
- val tincCfg = TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName)))
-
- Pair(
- openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase),
- openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase)
- )
+ TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName))).let { tincCfg ->
+ Pair(
+ TincKeyring.openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase),
+ TincKeyring.openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase))
+ }
} catch (e: FileNotFoundException) {
Pair(null, null)
} catch (e: PEMException) {
return reportError(resources.getString(R.string.message_could_not_decrypt_private_keys_format, e.message))
}
- val daemon = Tincd.start(netName, deviceFd!!.fd, privateKeys.first?.fd, privateKeys.second?.fd)
+ val daemon = Tincd.start(netName, deviceFd.fd, privateKeys.first?.fd, privateKeys.second?.fd)
setState(netName, interfaceCfg, deviceFd, daemon)
- Log.i(TAG, "tinc daemon started.")
+ waitForDaemonStartup().thenRun {
+ deviceFd.close()
+ Log.i(TAG, "tinc daemon started.")
+ broadcastEvent(Actions.EVENT_CONNECTED)
+ }
}
- private fun openPrivateKey(f: File?, passphrase: String?): ParcelFileDescriptor? {
- if (f == null || !f.exists() || passphrase == null) return null
-
- val pipe = ParcelFileDescriptor.createPipe()
- val decryptedKey = PemUtils.decrypt(PemUtils.read(f), passphrase)
- val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])
- PemUtils.write(decryptedKey, outputStream.writer())
- return pipe[0]
+ private fun stopVpn(): Unit = synchronized(this) {
+ Log.i(TAG, "Stopping any running tinc daemon.")
+ netName?.let {
+ Tinc.stop(it).thenRun {
+ Log.i(TAG, "All tinc daemons stopped.")
+ broadcastEvent(Actions.EVENT_DISCONNECTED)
+ setState(null, null, null, null)
+ }
+ }
}
private fun reportError(msg: String, e: Throwable? = null, docTopic: String? = null) {
@@ -108,10 +122,18 @@ class TincVpnService : VpnService() {
if (docTopic != null) resources.getString(R.string.app_doc_url_format, docTopic) else null)
}
- companion object {
+ private fun broadcastEvent(event: String) {
+ LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(event))
+ }
- val TAG = this::class.java.canonicalName!!
+ private fun waitForDaemonStartup() =
+ CompletableFuture
+ .runAsync { Thread.sleep(SETUP_DELAY) }
+ .thenCompose { netName?.let { Tinc.pid(it) } ?: CompletableFuture.completedFuture(0) }
+ companion object {
+ private const val SETUP_DELAY = 500L // ms
+ private val TAG = this::class.java.canonicalName!!
private var netName: String? = null
private var interfaceCfg: VpnInterfaceConfiguration? = null
private var fd: ParcelFileDescriptor? = null
@@ -119,35 +141,28 @@ class TincVpnService : VpnService() {
private fun setState(netName: String?, interfaceCfg: VpnInterfaceConfiguration?,
fd: ParcelFileDescriptor?, daemon: CompletableFuture?) {
+
TincVpnService.netName = netName
TincVpnService.interfaceCfg = interfaceCfg
TincVpnService.fd = fd
TincVpnService.daemon = daemon
}
- fun startVpn(netName: String, passphrase: String? = null) {
- App.getContext().startService(Intent(App.getContext(), TincVpnService::class.java)
- .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build()))
- }
-
- fun stopVpn() {
- try {
- Log.i(TAG, "Stopping any running tinc daemon.")
- if (netName != null) Tinc.stop(netName!!)
- daemon?.get()
- fd?.close()
- Log.i(TAG, "All tinc daemons stopped.")
- } catch (e: IOException) {
- Log.wtf(TAG, e)
- } finally {
- setState(null, null, null, null)
- }
- }
-
fun getCurrentNetName() = netName
fun getCurrentInterfaceCfg() = interfaceCfg
fun isConnected() = !(daemon?.isDone ?: true)
- }
+ fun connect(netName: String, passphrase: String? = null) {
+ App.getContext().startService(
+ Intent(App.getContext(), TincVpnService::class.java)
+ .setAction(Actions.ACTION_CONNECT)
+ .setData(Actions.buildNetworkUri(netName, passphrase)))
+ }
+ fun disconnect() {
+ App.getContext().startService(
+ Intent(App.getContext(), TincVpnService::class.java)
+ .setAction(Actions.ACTION_DISCONNECT))
+ }
+ }
}
diff --git a/app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt b/app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt
new file mode 100644
index 0000000..422763f
--- /dev/null
+++ b/app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt
@@ -0,0 +1,26 @@
+package org.pacien.tincapp.utils
+
+import android.os.ParcelFileDescriptor
+import org.pacien.tincapp.commands.TincApp
+import java.io.File
+import java.io.FileNotFoundException
+
+/**
+ * @author pacien
+ */
+object TincKeyring {
+ fun needsPassphrase(netName: String) = try {
+ TincApp.listPrivateKeys(netName).filter { it.exists() }.any { PemUtils.isEncrypted(PemUtils.read(it)) }
+ } catch (e: FileNotFoundException) {
+ false
+ }
+
+ fun openPrivateKey(f: File?, passphrase: String?): ParcelFileDescriptor? {
+ if (f == null || !f.exists() || passphrase == null) return null
+ val pipe = ParcelFileDescriptor.createPipe()
+ val decryptedKey = PemUtils.decrypt(PemUtils.read(f), passphrase)
+ val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])
+ PemUtils.write(decryptedKey, outputStream.writer())
+ return pipe[0]
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2849c8f..23469c0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -61,6 +61,7 @@
Unlock
Apply
Encrypt or decrypt private keys
+ Dismiss
No network configuration has been found.
No known node
@@ -75,6 +76,9 @@
Encrypting/decrypting private keys
Could not decrypt private keys:\n\n%1$s
Storage directory is unavailable.
+ Starting VPN…
+ Disconnecting VPN…
+ A passphrase is required to unlock the keyring.
none
yes
--
cgit v1.2.3