From dbba24ef009e5fa9ad428b0ba426b41672fa6b2e Mon Sep 17 00:00:00 2001
From: pacien
Date: Tue, 7 Aug 2018 00:19:57 +0200
Subject: Rename source directory
---
.../org/pacien/tincapp/activities/BaseActivity.kt | 114 ++++++++++
.../pacien/tincapp/activities/ConfigureActivity.kt | 145 +++++++++++++
.../org/pacien/tincapp/activities/StartActivity.kt | 201 ++++++++++++++++++
.../pacien/tincapp/activities/StatusActivity.kt | 196 +++++++++++++++++
.../pacien/tincapp/activities/ViewLogActivity.kt | 164 ++++++++++++++
.../kotlin/org/pacien/tincapp/commands/Command.kt | 46 ++++
.../kotlin/org/pacien/tincapp/commands/Executor.kt | 85 ++++++++
.../kotlin/org/pacien/tincapp/commands/Tinc.kt | 72 +++++++
.../kotlin/org/pacien/tincapp/commands/TincApp.kt | 74 +++++++
.../kotlin/org/pacien/tincapp/commands/Tincd.kt | 37 ++++
.../main/kotlin/org/pacien/tincapp/context/App.kt | 85 ++++++++
.../kotlin/org/pacien/tincapp/context/AppInfo.kt | 47 ++++
.../kotlin/org/pacien/tincapp/context/AppLogger.kt | 63 ++++++
.../tincapp/context/AppNotificationManager.kt | 84 ++++++++
.../kotlin/org/pacien/tincapp/context/AppPaths.kt | 71 +++++++
.../org/pacien/tincapp/context/CrashRecorder.kt | 48 +++++
.../kotlin/org/pacien/tincapp/data/CidrAddress.kt | 39 ++++
.../org/pacien/tincapp/data/TincConfiguration.kt | 41 ++++
.../tincapp/data/VpnInterfaceConfiguration.kt | 92 ++++++++
.../org/pacien/tincapp/extensions/Android.kt | 41 ++++
.../tincapp/extensions/ApacheConfiguration.kt | 33 +++
.../kotlin/org/pacien/tincapp/extensions/Java.kt | 38 ++++
.../pacien/tincapp/extensions/VpnServiceBuilder.kt | 80 +++++++
.../kotlin/org/pacien/tincapp/intent/Actions.kt | 39 ++++
.../org/pacien/tincapp/intent/BroadcastMapper.kt | 38 ++++
.../org/pacien/tincapp/service/TincVpnService.kt | 236 +++++++++++++++++++++
.../kotlin/org/pacien/tincapp/utils/PemUtils.kt | 94 ++++++++
.../org/pacien/tincapp/utils/ProgressModal.kt | 49 +++++
.../kotlin/org/pacien/tincapp/utils/TincKeyring.kt | 44 ++++
29 files changed, 2396 insertions(+)
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Executor.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Tinc.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/TincApp.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Tincd.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/App.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppInfo.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppLogger.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppNotificationManager.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppPaths.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/CrashRecorder.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/data/CidrAddress.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/data/TincConfiguration.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/data/VpnInterfaceConfiguration.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/Android.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/ApacheConfiguration.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/Java.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/VpnServiceBuilder.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/intent/Actions.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/intent/BroadcastMapper.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/service/TincVpnService.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/utils/PemUtils.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/utils/ProgressModal.kt
create mode 100644 app/src/main/kotlin/org/pacien/tincapp/utils/TincKeyring.kt
(limited to 'app/src/main/kotlin/org/pacien')
diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt
new file mode 100644
index 0000000..4dc2381
--- /dev/null
+++ b/app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt
@@ -0,0 +1,114 @@
+/*
+ * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
+ * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.activities
+
+import android.os.Bundle
+import android.support.annotation.StringRes
+import android.support.design.widget.Snackbar
+import android.support.v7.app.AlertDialog
+import android.support.v7.app.AppCompatActivity
+import android.view.Menu
+import android.view.MenuItem
+import kotlinx.android.synthetic.main.base.*
+import org.pacien.tincapp.R
+import org.pacien.tincapp.context.App
+import org.pacien.tincapp.context.AppInfo
+import org.pacien.tincapp.context.AppPaths
+import org.pacien.tincapp.context.CrashRecorder
+import org.pacien.tincapp.utils.ProgressModal
+
+/**
+ * @author pacien
+ */
+abstract class BaseActivity : AppCompatActivity() {
+ private var active = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.base)
+ }
+
+ override fun onCreateOptionsMenu(m: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_base, m)
+ return true
+ }
+
+ override fun onStart() {
+ super.onStart()
+ active = true
+ }
+
+ override fun onResume() {
+ super.onResume()
+ active = true
+ }
+
+ override fun onPause() {
+ active = false
+ super.onPause()
+ }
+
+ override fun onStop() {
+ active = false
+ super.onStop()
+ }
+
+ fun aboutDialog(@Suppress("UNUSED_PARAMETER") i: MenuItem) {
+ AlertDialog.Builder(this)
+ .setTitle(resources.getString(R.string.app_name))
+ .setMessage(resources.getString(R.string.app_short_desc) + "\n\n" +
+ resources.getString(R.string.app_copyright) + " " +
+ resources.getString(R.string.app_license) + "\n\n" +
+ AppInfo.all())
+ .setNeutralButton(R.string.action_open_project_website) { _, _ -> App.openURL(resources.getString(R.string.app_website_url)) }
+ .setPositiveButton(R.string.action_close) { _, _ -> Unit }
+ .show()
+ }
+
+ fun runOnUiThread(action: () -> Unit) {
+ if (active) super.runOnUiThread(action)
+ }
+
+ fun handleRecentCrash() {
+ if (!CrashRecorder.hasPreviouslyCrashed()) return
+ CrashRecorder.dismissPreviousCrash()
+
+ AlertDialog.Builder(this)
+ .setTitle(R.string.title_app_crash)
+ .setMessage(listOf(
+ resources.getString(R.string.message_app_crash),
+ resources.getString(R.string.message_crash_logged, AppPaths.appLogFile().absolutePath)
+ ).joinToString("\n\n"))
+ .setNeutralButton(R.string.action_send_report) { _, _ ->
+ App.sendMail(
+ resources.getString(R.string.app_dev_email),
+ listOf(R.string.app_name, R.string.title_app_crash).joinToString(" / ", transform = resources::getString),
+ AppPaths.appLogFile().let { if (it.exists()) it.readText() else "" })
+ }
+ .setPositiveButton(R.string.action_close) { _, _ -> Unit }
+ .show()
+ }
+
+ protected fun notify(@StringRes msg: Int) = Snackbar.make(activity_base, msg, Snackbar.LENGTH_LONG).show()
+ protected fun notify(msg: String) = Snackbar.make(activity_base, msg, Snackbar.LENGTH_LONG).show()
+ protected fun showProgressDialog(@StringRes msg: Int): AlertDialog = ProgressModal.show(this, getString(msg))
+ protected fun showErrorDialog(msg: String): AlertDialog = AlertDialog.Builder(this)
+ .setTitle(R.string.title_error).setMessage(msg)
+ .setPositiveButton(R.string.action_close) { _, _ -> Unit }.show()
+}
diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt
new file mode 100644
index 0000000..2aab304
--- /dev/null
+++ b/app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt
@@ -0,0 +1,145 @@
+/*
+ * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
+ * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.activities
+
+import android.content.Intent
+import android.os.Bundle
+import android.support.annotation.StringRes
+import android.support.v7.app.AlertDialog
+import android.view.View
+import com.google.zxing.integration.android.IntentIntegrator
+import com.google.zxing.integration.android.IntentResult
+import java8.util.concurrent.CompletableFuture
+import kotlinx.android.synthetic.main.base.*
+import kotlinx.android.synthetic.main.dialog_encrypt_decrypt_keys.view.*
+import kotlinx.android.synthetic.main.dialog_network_generate.view.*
+import kotlinx.android.synthetic.main.dialog_network_join.view.*
+import kotlinx.android.synthetic.main.page_configure.*
+import org.pacien.tincapp.R
+import org.pacien.tincapp.commands.Tinc
+import org.pacien.tincapp.commands.TincApp
+import org.pacien.tincapp.context.AppPaths
+import org.pacien.tincapp.extensions.Java.exceptionallyAccept
+import java.util.regex.Pattern
+
+/**
+ * @author pacien
+ */
+class ConfigureActivity : BaseActivity() {
+ companion object {
+ private val NETWORK_NAME_PATTERN = Pattern.compile("^[^\\x00/]*$")
+ }
+
+ private var joinDialog: View? = null
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
+ ?.let(IntentResult::getContents)
+ ?.let(String::trim)
+ ?.let { joinDialog?.invitation_url?.setText(it) }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+ layoutInflater.inflate(R.layout.page_configure, main_content)
+ writeContent()
+ }
+
+ fun scanCode(@Suppress("UNUSED_PARAMETER") v: View) {
+ IntentIntegrator(this).initiateScan()
+ }
+
+ fun openGenerateConfDialog(@Suppress("UNUSED_PARAMETER") v: View) {
+ val genDialog = layoutInflater.inflate(R.layout.dialog_network_generate, main_content, false)
+ AlertDialog.Builder(this).setTitle(R.string.title_new_network).setView(genDialog)
+ .setPositiveButton(R.string.action_create) { _, _ ->
+ generateConf(
+ genDialog.new_net_name.text.toString(),
+ genDialog.new_node_name.text.toString(),
+ genDialog.new_passphrase.text.toString())
+ }.setNegativeButton(R.string.action_cancel) { _, _ -> Unit }.show()
+ }
+
+ fun openJoinNetworkDialog(@Suppress("UNUSED_PARAMETER") v: View) {
+ joinDialog = layoutInflater.inflate(R.layout.dialog_network_join, main_content, false)
+ AlertDialog.Builder(this).setTitle(R.string.title_join_network).setView(joinDialog)
+ .setPositiveButton(R.string.action_join) { _, _ ->
+ joinNetwork(
+ joinDialog!!.net_name.text.toString(),
+ joinDialog!!.invitation_url.text.toString(),
+ joinDialog!!.join_passphrase.text.toString())
+ }.setNegativeButton(R.string.action_cancel) { _, _ -> Unit }.show()
+ }
+
+ fun openEncryptDecryptPrivateKeyDialog(@Suppress("UNUSED_PARAMETER") v: View) {
+ val encryptDecryptDialog = layoutInflater.inflate(R.layout.dialog_encrypt_decrypt_keys, main_content, false)
+ AlertDialog.Builder(this).setTitle(R.string.title_private_keys_encryption).setView(encryptDecryptDialog)
+ .setPositiveButton(R.string.action_apply) { _, _ ->
+ encryptDecryptPrivateKeys(
+ encryptDecryptDialog!!.enc_dec_net_name.text.toString(),
+ encryptDecryptDialog.enc_dec_current_passphrase.text.toString(),
+ encryptDecryptDialog.enc_dec_new_passphrase.text.toString())
+ }.setNegativeButton(R.string.action_cancel) { _, _ -> Unit }.show()
+ }
+
+ private fun writeContent() {
+ text_configuration_directory.text = AppPaths.confDir().absolutePath
+ text_log_directory.text = AppPaths.cacheDir().absolutePath
+ text_tinc_binary.text = AppPaths.tinc().absolutePath
+ }
+
+ private fun generateConf(netName: String, nodeName: String, passphrase: String? = null) = execAction(
+ R.string.message_generating_configuration,
+ validateNetName(netName)
+ .thenCompose { Tinc.init(netName, nodeName) }
+ .thenCompose { TincApp.removeScripts(netName) }
+ .thenCompose { TincApp.generateIfaceCfgTemplate(netName) }
+ .thenCompose { TincApp.setPassphrase(netName, newPassphrase = passphrase) })
+
+ private fun joinNetwork(netName: String, url: String, passphrase: String? = null) = execAction(
+ R.string.message_joining_network,
+ validateNetName(netName)
+ .thenCompose { Tinc.join(netName, url) }
+ .thenCompose { TincApp.removeScripts(netName) }
+ .thenCompose { TincApp.generateIfaceCfg(netName) }
+ .thenCompose { TincApp.setPassphrase(netName, newPassphrase = passphrase) })
+
+ private fun encryptDecryptPrivateKeys(netName: String, currentPassphrase: String, newPassphrase: String) = execAction(
+ R.string.message_encrypting_decrypting_private_keys,
+ validateNetName(netName)
+ .thenCompose { TincApp.setPassphrase(netName, currentPassphrase, newPassphrase) })
+
+ private fun execAction(@StringRes label: Int, action: CompletableFuture) {
+ showProgressDialog(label).let { progressDialog ->
+ action
+ .whenComplete { _, _ -> progressDialog.dismiss() }
+ .thenAccept { notify(R.string.message_network_configuration_written) }
+ .exceptionallyAccept { runOnUiThread { showErrorDialog(it.cause!!.localizedMessage) } }
+ }
+ }
+
+ private fun validateNetName(netName: String): CompletableFuture =
+ if (NETWORK_NAME_PATTERN.matcher(netName).matches())
+ CompletableFuture.completedFuture(Unit)
+ else
+ CompletableFuture.failedFuture(IllegalArgumentException(resources.getString(R.string.message_invalid_network_name)))
+}
diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt
new file mode 100644
index 0000000..1e267d6
--- /dev/null
+++ b/app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt
@@ -0,0 +1,201 @@
+/*
+ * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
+ * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.activities
+
+import android.app.Activity
+import android.content.Intent
+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
+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.BroadcastMapper
+import org.pacien.tincapp.service.TincVpnService
+import org.pacien.tincapp.utils.TincKeyring
+
+/**
+ * @author pacien
+ */
+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
+
+ fun tryStart(netName: String? = null, passphrase: String? = null, displayStatus: Boolean? = null) {
+ if (netName != null) this.netName = netName
+ 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 broadcastMapper = BroadcastMapper(mapOf(
+ Actions.EVENT_CONNECTED to this::onVpnStart,
+ Actions.EVENT_ABORTED to this::onVpnStartError))
+
+ private var connectDialog: AlertDialog? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ 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 {
+ menuInflater.inflate(R.menu.menu_start, m)
+ return super.onCreateOptionsMenu(m)
+ }
+
+ override fun onDestroy() {
+ networkList.destroy()
+ connectDialog?.dismiss()
+ super.onDestroy()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ networkList.onRefresh()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (TincVpnService.isConnected()) openStatusActivity(false)
+ broadcastMapper.register()
+ handleRecentCrash()
+ }
+
+ override fun onPause() {
+ broadcastMapper.unregister()
+ super.onPause()
+ }
+
+ 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))
+
+ private fun onVpnStart() {
+ connectDialog?.dismiss()
+ if (connectionStarter.displayStatus()) openStatusActivity()
+ finish()
+ }
+
+ private fun onVpnStartError() {
+ connectDialog?.dismiss()
+ }
+
+ private fun openStatusActivity(transition: Boolean = true) =
+ startActivity(
+ Intent(this, StatusActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .apply { if (!transition) addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) })
+}
diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt
new file mode 100644
index 0000000..68e008e
--- /dev/null
+++ b/app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt
@@ -0,0 +1,196 @@
+/*
+ * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
+ * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.activities
+
+import android.content.Intent
+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
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.TextView
+import java8.util.concurrent.CompletableFuture
+import kotlinx.android.synthetic.main.base.*
+import kotlinx.android.synthetic.main.dialog_node_details.view.*
+import kotlinx.android.synthetic.main.fragment_list_view.*
+import kotlinx.android.synthetic.main.fragment_network_status_header.*
+import org.pacien.tincapp.R
+import org.pacien.tincapp.commands.Executor
+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.BroadcastMapper
+import org.pacien.tincapp.service.TincVpnService
+import java.util.*
+import kotlin.concurrent.timerTask
+
+/**
+ * @author pacien
+ */
+class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener {
+ private val broadcastMapper = BroadcastMapper(mapOf(Actions.EVENT_DISCONNECTED to this::onVpnShutdown))
+ private var shutdownDialog: AlertDialog? = null
+ private var nodeListAdapter: ArrayAdapter? = null
+ private var refreshTimer: Timer? = null
+ private var listNetworksAfterExit = true
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ nodeListAdapter = 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_status_header, list, false), null, false)
+ 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 {
+ menuInflater.inflate(R.menu.menu_status, m)
+ return super.onCreateOptionsMenu(m)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ nodeListAdapter = null
+ refreshTimer = null
+ }
+
+ override fun onStart() {
+ super.onStart()
+ refreshTimer = Timer(true)
+ refreshTimer?.schedule(timerTask { updateView() }, NOW, REFRESH_RATE)
+ writeNetworkInfo(TincVpnService.getCurrentInterfaceCfg() ?: VpnInterfaceConfiguration())
+ }
+
+ override fun onStop() {
+ refreshTimer?.cancel()
+ super.onStop()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ broadcastMapper.register()
+ updateView()
+ handleRecentCrash()
+ }
+
+ override fun onPause() {
+ broadcastMapper.unregister()
+ super.onPause()
+ }
+
+ override fun onRefresh() {
+ refreshTimer?.schedule(timerTask { updateView() }, NOW)
+ }
+
+ override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = when (view) {
+ is TextView -> showNodeInfo(view.text.toString())
+ else -> Unit
+ }
+
+ 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()
+ }
+
+ fun openLogViewer(@Suppress("UNUSED_PARAMETER") i: MenuItem) =
+ startActivity(Intent(this, ViewLogActivity::class.java))
+
+ 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() })
+ text_network_dns_servers.setText(cfg.dnsServers)
+ text_network_search_domains.setText(cfg.searchDomains)
+ text_network_allow_bypass.text = getString(if (cfg.allowBypass) R.string.value_yes else R.string.value_no)
+ block_network_allowed_applications.visibility = if (cfg.allowedApplications.isNotEmpty()) View.VISIBLE else View.GONE
+ text_network_allowed_applications.setText(cfg.allowedApplications)
+ block_network_disallowed_applications.visibility = if (cfg.disallowedApplications.isNotEmpty()) View.VISIBLE else View.GONE
+ text_network_disallowed_applications.setText(cfg.disallowedApplications)
+ }
+
+ private fun writeNodeList(nodeList: List) {
+ nodeListAdapter?.setElements(nodeList)
+ node_list_placeholder.visibility = View.GONE
+ list_wrapper.isRefreshing = false
+ }
+
+ private fun updateNodeList() {
+ getNodeNames().thenAccept { nodeList -> runOnUiThread { writeNodeList(nodeList) } }
+ }
+
+ private fun showNodeInfo(nodeName: String) {
+ val dialogTextView = layoutInflater.inflate(R.layout.dialog_node_details, main_content, false)
+
+ runOnUiThread {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.title_node_info)
+ .setView(dialogTextView)
+ .setPositiveButton(R.string.action_close) { _, _ -> Unit }
+ .show()
+ }
+
+ TincVpnService.getCurrentNetName()?.let { netName ->
+ Tinc.info(netName, nodeName).thenAccept { nodeInfo ->
+ runOnUiThread { dialogTextView.dialog_node_details.text = nodeInfo }
+ }
+ }
+ }
+
+ private fun updateView() = when {
+ TincVpnService.isConnected() -> updateNodeList()
+ else -> openStartActivity()
+ }
+
+ private fun openStartActivity() {
+ startActivity(Intent(this, StartActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
+ finish()
+ }
+
+ companion object {
+ private const val REFRESH_RATE = 5000L
+ private const val NOW = 0L
+
+ fun getNodeNames(): CompletableFuture> = TincVpnService.getCurrentNetName()?.let { netName ->
+ Tinc.dumpNodes(netName).thenApply> { list -> list.map { it.substringBefore(' ') } }
+ } ?: Executor.supplyAsyncTask> { emptyList() }
+ }
+}
diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt
new file mode 100644
index 0000000..f3f7e24
--- /dev/null
+++ b/app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt
@@ -0,0 +1,164 @@
+/*
+ * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
+ * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.activities
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.ScrollView
+import kotlinx.android.synthetic.main.base.*
+import kotlinx.android.synthetic.main.page_viewlog.*
+import org.pacien.tincapp.R
+import org.pacien.tincapp.commands.Executor
+import org.pacien.tincapp.commands.Tinc
+import org.pacien.tincapp.service.TincVpnService
+import java.util.*
+import kotlin.concurrent.timer
+
+/**
+ * @author pacien
+ */
+class ViewLogActivity : BaseActivity() {
+ companion object {
+ private const val LOG_LINES = 250
+ private const val LOG_LEVEL = 5
+ private const val NEW_LINE = "\n"
+ private const val SPACED_NEW_LINE = "\n\n"
+ private const val UPDATE_INTERVAL = 250L // ms
+ private const val MIME_TYPE = "text/plain"
+ }
+
+ private val log = LinkedList()
+ private var logUpdateTimer: Timer? = null
+ private var logger: Process? = null
+ private var toggleButton: MenuItem? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+ layoutInflater.inflate(R.layout.page_viewlog, main_content)
+ toggleLogging(true)
+ }
+
+ override fun onCreateOptionsMenu(m: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_viewlog, m)
+ toggleButton = m.findItem(R.id.log_viewer_action_toggle)
+ return super.onCreateOptionsMenu(m)
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ finish()
+ return true
+ }
+
+ override fun onDestroy() {
+ toggleLogging(false)
+ super.onDestroy()
+ }
+
+ fun share(@Suppress("UNUSED_PARAMETER") menuItem: MenuItem) {
+ synchronized(this) {
+ val logFragment = log.joinToString(NEW_LINE)
+ val shareIntent = Intent(Intent.ACTION_SEND)
+ .setType(MIME_TYPE)
+ .putExtra(Intent.EXTRA_TEXT, logFragment)
+
+ startActivity(Intent.createChooser(shareIntent, resources.getString(R.string.menu_share_log)))
+ }
+ }
+
+ fun toggleLogging(@Suppress("UNUSED_PARAMETER") menuItem: MenuItem) = toggleLogging(logger == null)
+
+ private fun toggleLogging(enable: Boolean) {
+ if (enable) {
+ disableUserScroll()
+ toggleButton?.setIcon(R.drawable.ic_pause_circle_outline_primary_24dp)
+ startLogging()
+ } else {
+ enableUserScroll()
+ toggleButton?.setIcon(R.drawable.ic_pause_circle_filled_primary_24dp)
+ stopLogging()
+ }
+ }
+
+ private fun startLogging(level: Int = LOG_LEVEL) {
+ appendLog(resources.getString(R.string.message_log_level_set, level))
+
+ TincVpnService.getCurrentNetName()?.let { netName ->
+ Tinc.log(netName, level).let { process ->
+ logger = process
+ Executor.runAsyncTask { captureLog(process) }
+ }
+ logUpdateTimer = timer(period = UPDATE_INTERVAL, action = { printLog() })
+ } ?: run {
+ appendLog(resources.getString(R.string.message_no_daemon))
+ toggleLogging(false)
+ }
+ }
+
+ private fun stopLogging() {
+ logger?.destroy()
+ logger = null
+ logUpdateTimer?.cancel()
+ logUpdateTimer?.purge()
+ logUpdateTimer = null
+ appendLog(resources.getString(R.string.message_log_paused))
+ printLog()
+ }
+
+ private fun captureLog(logger: Process) {
+ logger.inputStream?.use { inputStream ->
+ inputStream.bufferedReader().useLines { lines ->
+ lines.forEach { appendLog(it) }
+ }
+ }
+ }
+
+ private fun appendLog(line: String) = synchronized(this) {
+ if (log.size >= LOG_LINES) log.removeFirst()
+ log.addLast(line)
+ }
+
+ private fun printLog() = synchronized(this) {
+ log.joinToString(SPACED_NEW_LINE).let {
+ logview_text.post {
+ logview_text.text = it
+ logview_frame.post { logview_frame.fullScroll(View.FOCUS_DOWN) }
+ }
+ }
+ }
+
+ private fun enableUserScroll() {
+ logview_text.setTextIsSelectable(true)
+ logview_frame.setState(true)
+ }
+
+ private fun disableUserScroll() {
+ logview_text.setTextIsSelectable(false)
+ logview_frame.setState(false)
+ }
+
+ private fun ScrollView.setState(enabled: Boolean) {
+ if (enabled) setOnTouchListener(null) else setOnTouchListener { _, _ -> true }
+ logview_frame.isSmoothScrollingEnabled = enabled
+ logview_frame.isVerticalScrollBarEnabled = enabled
+ }
+}
diff --git a/app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt b/app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt
new file mode 100644
index 0000000..132cda9
--- /dev/null
+++ b/app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt
@@ -0,0 +1,46 @@
+/*
+ * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
+ * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.commands
+
+import java.util.*
+
+/**
+ * @author pacien
+ */
+internal class Command(private val cmd: String) {
+ private data class Option(val key: String, val value: String?) {
+ fun toCommandLineOption(): String = if (value != null) "--$key=$value" else "--$key"
+ }
+
+ private val opts: MutableList