From 91adc78116f074f0a50bfdcc2069382677ea05cf Mon Sep 17 00:00:00 2001 From: pacien Date: Tue, 22 Aug 2017 14:14:59 +0200 Subject: Better error handling --- app/src/main/AndroidManifest.xml | 3 +- .../org/pacien/tincapp/activities/BaseActivity.kt | 12 +-- .../pacien/tincapp/activities/ConfigureActivity.kt | 5 +- .../pacien/tincapp/activities/LaunchActivity.kt | 62 ++++++++++++ .../pacien/tincapp/activities/PromptActivity.kt | 63 ------------- .../org/pacien/tincapp/activities/StartActivity.kt | 2 +- .../main/java/org/pacien/tincapp/context/App.kt | 21 +++++ .../java/org/pacien/tincapp/context/AppPaths.kt | 3 + .../java/org/pacien/tincapp/data/CidrAddress.kt | 8 +- .../org/pacien/tincapp/intent/action/Actions.kt | 3 - .../org/pacien/tincapp/service/TincVpnService.kt | 105 +++++++++++++-------- app/src/main/res/values/strings.xml | 7 ++ 12 files changed, 176 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt delete mode 100644 app/src/main/java/org/pacien/tincapp/activities/PromptActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5611e4c..dc406f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + diff --git a/app/src/main/java/org/pacien/tincapp/activities/BaseActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/BaseActivity.kt index 000320c..4904a66 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/BaseActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/BaseActivity.kt @@ -1,8 +1,6 @@ package org.pacien.tincapp.activities import android.app.ProgressDialog -import android.content.Intent -import android.net.Uri import android.os.Bundle import android.support.annotation.StringRes import android.support.design.widget.Snackbar @@ -13,6 +11,7 @@ import android.view.MenuItem import kotlinx.android.synthetic.main.base.* import org.pacien.tincapp.BuildConfig import org.pacien.tincapp.R +import org.pacien.tincapp.context.App import org.pacien.tincapp.context.AppInfo /** @@ -38,19 +37,16 @@ abstract class BaseActivity : AppCompatActivity() { resources.getString(R.string.app_copyright) + " " + resources.getString(R.string.app_license) + "\n\n" + AppInfo.all()) - .setNeutralButton(R.string.action_open_project_website) { _, _ -> openWebsite(R.string.app_website_url) } - .setPositiveButton(R.string.action_close, dismiss) + .setNeutralButton(R.string.action_open_project_website) { _, _ -> App.openURL(resources.getString(R.string.app_website_url)) } + .setPositiveButton(R.string.action_close, App.dismissAction) .show() } - protected fun openWebsite(@StringRes url: Int) = startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(resources.getString(url)))) 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): ProgressDialog = ProgressDialog.show(this, null, getString(msg), true, false) protected fun showErrorDialog(msg: String): AlertDialog = AlertDialog.Builder(this) .setTitle(R.string.title_error).setMessage(msg) - .setPositiveButton(R.string.action_close, dismiss).show() - - protected val dismiss = { _: Any, _: Any -> /* nop */ } + .setPositiveButton(R.string.action_close, App.dismissAction).show() } diff --git a/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt index b030a85..ea37944 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/ConfigureActivity.kt @@ -13,6 +13,7 @@ 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.App import org.pacien.tincapp.context.AppPaths import org.pacien.tincapp.extensions.Java.exceptionallyAccept @@ -43,7 +44,7 @@ class ConfigureActivity : BaseActivity() { AlertDialog.Builder(this).setTitle(R.string.title_new_network).setView(dialogFrame) .setPositiveButton(R.string.action_create) { _, _ -> generateConf(netNameField.text.toString(), nodeNameField.text.toString()) } - .setNegativeButton(R.string.action_cancel, dismiss).show() + .setNegativeButton(R.string.action_cancel, App.dismissAction).show() } fun openJoinNetworkDialog(@Suppress("UNUSED_PARAMETER") v: View) { @@ -61,7 +62,7 @@ class ConfigureActivity : BaseActivity() { AlertDialog.Builder(this).setTitle(R.string.title_join_network).setView(dialogFrame) .setPositiveButton(R.string.action_join) { _, _ -> joinNetwork(netNameField.text.toString(), joinUrlField.text.toString()) } - .setNegativeButton(R.string.action_cancel, dismiss).show() + .setNegativeButton(R.string.action_cancel, App.dismissAction).show() } private fun writeContent() { diff --git a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt new file mode 100644 index 0000000..6eb630d --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt @@ -0,0 +1,62 @@ +package org.pacien.tincapp.activities + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.net.VpnService +import android.os.Bundle +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 + +/** + * @author pacien + */ +class LaunchActivity : Activity() { + + 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 (result == Activity.RESULT_OK) TincVpnService.startVpn(intent.data.schemeSpecificPart) + finish() + } + + private fun requestPerm() = VpnService.prepare(this).let { + if (it != null) + startActivityForResult(it, 0) + else + onActivityResult(0, Activity.RESULT_OK, null) + } + + private fun disconnect() { + TincVpnService.stopVpn() + finish() + } + + companion object { + + fun connect(netName: String) { + 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).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/PromptActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/PromptActivity.kt deleted file mode 100644 index 6310d63..0000000 --- a/app/src/main/java/org/pacien/tincapp/activities/PromptActivity.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.pacien.tincapp.activities - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.net.VpnService -import android.os.Bundle -import org.pacien.tincapp.BuildConfig -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 - -/** - * @author pacien - */ -class PromptActivity : Activity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - when (intent.action) { - ACTION_CONNECT -> connect() - ACTION_DISCONNECT -> disconnect() - } - } - - override fun onActivityResult(request: Int, result: Int, data: Intent?) { - if (result == Activity.RESULT_OK) TincVpnService.startVpn(intent.data.schemeSpecificPart) - finish() - } - - private fun connect() = VpnService.prepare(this).let { - if (it != null) - startActivityForResult(it, 0) - else - onActivityResult(0, Activity.RESULT_OK, null) - } - - private fun disconnect() { - TincVpnService.stopVpn() - finish() - } - - companion object { - - fun connect(netName: String) { - App.getContext().startActivity(Intent(App.getContext(), PromptActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setAction(ACTION_CONNECT) - .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).build())) - } - - fun disconnect() { - App.getContext().startActivity(Intent(App.getContext(), PromptActivity::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 e49e261..6ef8974 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt @@ -77,7 +77,7 @@ class StartActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefr } override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = - PromptActivity.connect((view as TextView).text.toString()) + LaunchActivity.connect((view as TextView).text.toString()) fun openConfigureActivity(@Suppress("UNUSED_PARAMETER") i: MenuItem) = startActivity(Intent(this, ConfigureActivity::class.java)) diff --git a/app/src/main/java/org/pacien/tincapp/context/App.kt b/app/src/main/java/org/pacien/tincapp/context/App.kt index 4b7e44e..7aaa3d0 100644 --- a/app/src/main/java/org/pacien/tincapp/context/App.kt +++ b/app/src/main/java/org/pacien/tincapp/context/App.kt @@ -2,6 +2,12 @@ package org.pacien.tincapp.context import android.app.Application import android.content.Context +import android.content.Intent +import android.net.Uri +import android.support.annotation.StringRes +import android.support.v7.app.AlertDialog +import android.view.WindowManager +import org.pacien.tincapp.R /** * @author pacien @@ -14,9 +20,24 @@ class App : Application() { } companion object { + private var appContext: Context? = null + fun getContext() = appContext!! fun getResources() = getContext().resources!! + + fun alert(@StringRes title: Int, msg: String, manualLink: String? = null) = + AlertDialog.Builder(getContext(), R.style.Theme_AppCompat_Dialog) + .setTitle(title).setMessage(msg) + .apply { if (manualLink != null) setNeutralButton(R.string.action_open_manual) { _, _ -> openURL(manualLink) } } + .setPositiveButton(R.string.action_close, dismissAction) + .create().apply { window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR) }.show() + + fun openURL(url: String) = + appContext?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + + val dismissAction = { _: Any, _: Any -> /* nop */ } + } } diff --git a/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt b/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt index c745d4d..673faa7 100644 --- a/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt +++ b/app/src/main/java/org/pacien/tincapp/context/AppPaths.kt @@ -1,6 +1,7 @@ package org.pacien.tincapp.context import java.io.File +import java.io.FileNotFoundException /** * @author pacien @@ -30,6 +31,8 @@ object AppPaths { fun logFile(netName: String) = File(cacheDir(), String.format(LOGFILE_FORMAT, netName)) fun pidFile(netName: String) = File(App.getContext().cacheDir, String.format(PIDFILE_FORMAT, netName)) + fun existing(f: File) = f.apply { if (!exists()) throw FileNotFoundException(f.absolutePath) } + fun tincd() = File(binDir(), TINCD_BIN) fun tinc() = File(binDir(), TINC_BIN) diff --git a/app/src/main/java/org/pacien/tincapp/data/CidrAddress.kt b/app/src/main/java/org/pacien/tincapp/data/CidrAddress.kt index bce9894..273b5a2 100644 --- a/app/src/main/java/org/pacien/tincapp/data/CidrAddress.kt +++ b/app/src/main/java/org/pacien/tincapp/data/CidrAddress.kt @@ -1,5 +1,7 @@ package org.pacien.tincapp.data +import org.apache.commons.configuration2.ex.ConversionException + /** * @author pacien */ @@ -9,7 +11,11 @@ data class CidrAddress(val address: String, val prefix: Int) { private val SEPARATOR = "/" - fun fromSlashSeparated(s: String) = CidrAddress(s.substringBefore(SEPARATOR), Integer.parseInt(s.substringAfter(SEPARATOR))) + fun fromSlashSeparated(s: String) = try { + CidrAddress(s.substringBefore(SEPARATOR), Integer.parseInt(s.substringAfter(SEPARATOR))) + } catch (e: Exception) { + throw ConversionException(e.message, e) + } } 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 index b210e14..ece9b68 100644 --- a/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt +++ b/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt @@ -11,7 +11,4 @@ private val PREFIX = "${BuildConfig.APPLICATION_ID}.intent.action" val ACTION_CONNECT = "$PREFIX.CONNECT" val ACTION_DISCONNECT = "$PREFIX.DISCONNECT" -val ACTION_START_SERVICE = "$PREFIX.START_SERVICE" -val ACTION_STOP_SERVICE = "$PREFIX.STOP_SERVICE" - 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 12ac17f..cd1dd74 100644 --- a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt +++ b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt @@ -5,7 +5,10 @@ import android.content.Intent import android.net.Uri import android.net.VpnService import android.os.ParcelFileDescriptor +import android.util.Log +import org.apache.commons.configuration2.ex.ConversionException import org.pacien.tincapp.BuildConfig +import org.pacien.tincapp.R import org.pacien.tincapp.commands.Tinc import org.pacien.tincapp.commands.Tincd import org.pacien.tincapp.context.App @@ -13,9 +16,8 @@ import org.pacien.tincapp.context.AppPaths 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.ACTION_START_SERVICE -import org.pacien.tincapp.intent.action.ACTION_STOP_SERVICE import org.pacien.tincapp.intent.action.TINC_SCHEME +import java.io.FileNotFoundException import java.io.IOException /** @@ -23,65 +25,90 @@ import java.io.IOException */ class TincVpnService : VpnService() { - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - when (intent.action) { - ACTION_START_SERVICE -> startVpn(intent.data.schemeSpecificPart) - ACTION_STOP_SERVICE -> onDestroy() - } - - return Service.START_STICKY + override fun onDestroy() { + stopVpn() + super.onDestroy() } - override fun onDestroy() { - connected = false - - try { - if (netName != null) Tinc.stop(netName!!) - fd?.close() - } catch (e: IOException) { - e.printStackTrace() - } finally { - netName = null - interfaceCfg = null - fd = null - super.onDestroy() - } + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if (isConnected()) stopVpn() + startVpn(intent.data.schemeSpecificPart) + return Service.START_REDELIVER_INTENT } private fun startVpn(netName: String) { - if (isConnected()) onDestroy() - TincVpnService.netName = netName - TincVpnService.interfaceCfg = VpnInterfaceConfiguration.fromIfaceConfiguration(AppPaths.netConfFile(netName)) - - val net = Builder().setSession(netName).applyCfg(TincVpnService.interfaceCfg!!) - applyIgnoringException(net::addDisallowedApplication, BuildConfig.APPLICATION_ID) - - try { - fd = net.establish() - Tincd.start(netName, fd!!.fd) - } catch (e: IOException) { - e.printStackTrace() + if (netName.isBlank()) + return reportError(resources.getString(R.string.message_no_network_name_provided), docTopic = "intent-api") + + if (!AppPaths.confDir(netName).exists()) + return reportError(resources.getString(R.string.message_no_configuration_for_network_format, netName), docTopic = "configuration") + + Log.i(TAG, "Starting tinc daemon for network \"$netName\".") + + val interfaceCfg = try { + VpnInterfaceConfiguration.fromIfaceConfiguration(AppPaths.existing(AppPaths.netConfFile(netName))) + } catch (e: FileNotFoundException) { + return reportError(resources.getString(R.string.message_network_config_not_found_format, e.message!!), e, "configuration") + } catch (e: ConversionException) { + return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface") } - connected = true + val fd = try { + Builder().setSession(netName) + .applyCfg(interfaceCfg) + .also { applyIgnoringException(it::addDisallowedApplication, BuildConfig.APPLICATION_ID) } + .establish() + } catch (e: IllegalArgumentException) { + return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface") + } + + Tincd.start(netName, fd!!.fd) + setState(true, netName, interfaceCfg, fd) + Log.i(TAG, "tinc daemon started.") + } + + private fun reportError(msg: String, e: Throwable? = null, docTopic: String? = null) { + if (e != null) + Log.e(TAG, msg, e) + else + Log.e(TAG, msg) + + App.alert(R.string.title_unable_to_start_tinc, msg, + if (docTopic != null) resources.getString(R.string.app_doc_url_format, docTopic) else null) } companion object { + val TAG = this::class.java.canonicalName!! + private var connected: Boolean = false private var netName: String? = null private var interfaceCfg: VpnInterfaceConfiguration? = null private var fd: ParcelFileDescriptor? = null + private fun setState(connected: Boolean, netName: String?, interfaceCfg: VpnInterfaceConfiguration?, fd: ParcelFileDescriptor?) { + TincVpnService.connected = connected + TincVpnService.netName = netName + TincVpnService.interfaceCfg = interfaceCfg + TincVpnService.fd = fd + } + fun startVpn(netName: String) { App.getContext().startService(Intent(App.getContext(), TincVpnService::class.java) - .setAction(ACTION_START_SERVICE) .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).build())) } fun stopVpn() { - App.getContext().startService(Intent(App.getContext(), TincVpnService::class.java) - .setAction(ACTION_STOP_SERVICE)) + try { + Log.i(TAG, "Stopping any running tinc daemon.") + if (netName != null) Tinc.stop(netName!!) + fd?.close() + Log.i(TAG, "All tinc daemons stopped.") + } catch (e: IOException) { + Log.wtf(TAG, e) + } finally { + setState(false, null, null, null) + } } fun getCurrentNetName() = netName diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85b5172..70a13da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ Copyright 2017 Pacien. Software distributed under the terms of the GNU General Public License v3. http://tincapp.pacien.org + http://tincapp.pacien.org/doc.html#%1$s App version %1$s (%2$s build) Running on Android %1$s %2$s @@ -39,6 +40,7 @@ Error New network Join network + Unable to start tinc Close Cancel @@ -47,12 +49,17 @@ Join network via invitation URL Create Join + Open manual No network configuration has been found. No known node Generating node configuration… Joining network… Network configuration successfully created. + No network name has been provided. + No configuration has been found for network \"%1$s\". + Network configuration file not found at \"%1$s\". + Invalid network configuration:\n\n%1$s none yes -- cgit v1.2.3