diff --git a/android/build.gradle b/android/build.gradle
index c808709..45d4503 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,8 +37,8 @@ android {
defaultConfig {
applicationId "com.nutomic.ensichat"
targetSdkVersion 23
- versionCode 8
- versionName "0.1.7"
+ versionCode project.properties.versionCode.toInteger()
+ versionName project.properties.versionName
multiDexEnabled true
testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 3c8e103..dd10ed1 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -6,12 +6,14 @@
+
+
+
+
+
+
+
+
diff --git a/android/src/main/scala/com/nutomic/ensichat/App.scala b/android/src/main/scala/com/nutomic/ensichat/App.scala
new file mode 100644
index 0000000..f133d60
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/App.scala
@@ -0,0 +1,15 @@
+package com.nutomic.ensichat
+
+import android.support.multidex.MultiDexApplication
+import com.nutomic.ensichat.core.interfaces.Log
+import com.nutomic.ensichat.util.{Logging, PRNGFixes}
+
+class App extends MultiDexApplication {
+
+ override def onCreate(): Unit = {
+ super.onCreate()
+ Log.setLogInstance(new Logging())
+ PRNGFixes.apply()
+ }
+
+}
\ No newline at end of file
diff --git a/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
index 936baf6..ff271ee 100644
--- a/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
@@ -19,8 +19,6 @@ import com.nutomic.ensichat.views.UsersAdapter
*/
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
- private val Tag = "AddContactsActivity"
-
private lazy val database = new Database(this)
private lazy val adapter = new UsersAdapter(this)
diff --git a/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
index 86905a6..40d826b 100644
--- a/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
@@ -11,8 +11,8 @@ import android.view.{KeyEvent, View}
import android.widget.TextView.OnEditorActionListener
import android.widget.{Button, EditText, TextView}
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.core.interfaces.Settings
-import com.nutomic.ensichat.core.interfaces.Settings._
+import com.nutomic.ensichat.core.interfaces.SettingsInterface
+import com.nutomic.ensichat.core.interfaces.SettingsInterface._
/**
* Shown on first start, lets the user enter their name.
@@ -69,11 +69,11 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
preferences
.edit()
.putBoolean(KeyIsFirstStart, false)
- .putString(Settings.KeyUserName, username.getText.toString.trim)
- .putString(Settings.KeyUserStatus, Settings.DefaultUserStatus)
- .putBoolean(Settings.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
- .putString(Settings.KeyScanInterval, DefaultScanInterval.toString)
- .putString(Settings.KeyMaxConnections, DefaultMaxConnections.toString)
+ .putString(SettingsInterface.KeyUserName, username.getText.toString.trim)
+ .putString(SettingsInterface.KeyUserStatus, SettingsInterface.DefaultUserStatus)
+ .putBoolean(SettingsInterface.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
+ .putString(SettingsInterface.KeyScanInterval, DefaultScanInterval.toString)
+ .putString(SettingsInterface.KeyMaxConnections, DefaultMaxConnections.toString)
.apply()
startMainActivity()
diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala
similarity index 89%
rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala
index 639cfaf..100f1e8 100644
--- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala
@@ -8,7 +8,7 @@ import android.util.Log
/**
* Attempts to connect to another device and calls [[onConnected]] on success.
*/
-class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
+class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
private val Tag = "ConnectThread"
diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothDevice.scala
similarity index 100%
rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothDevice.scala
diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
index f2a83c0..12b488d 100644
--- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
@@ -9,7 +9,7 @@ import android.preference.PreferenceManager
import android.util.Log
import com.nutomic.ensichat.R
import com.nutomic.ensichat.core.body.ConnectionInfo
-import com.nutomic.ensichat.core.interfaces.{Settings, TransmissionInterface}
+import com.nutomic.ensichat.core.interfaces.{SettingsInterface, TransmissionInterface}
import com.nutomic.ensichat.core.{Address, ConnectionHandler, Message}
import com.nutomic.ensichat.service.ChatService
@@ -38,9 +38,9 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
private var devices = new HashMap[Device.ID, Device]()
- private var connections = new HashMap[Device.ID, TransferThread]()
+ private var connections = new HashMap[Device.ID, BluetoothTransferThread]()
- private var listenThread: Option[ListenThread] = None
+ private var listenThread: Option[BluetoothListenThread] = None
private var cancelDiscovery = false
@@ -86,7 +86,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
* Starts discovery and listening.
*/
private def startBluetoothConnections(): Unit = {
- listenThread = Some(new ListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
+ listenThread = Some(new BluetoothListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
listenThread.get.start()
cancelDiscovery = false
discover()
@@ -106,7 +106,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
val pm = PreferenceManager.getDefaultSharedPreferences(context)
val scanInterval =
- pm.getString(Settings.KeyScanInterval, Settings.DefaultScanInterval.toString).toInt * 1000
+ pm.getString(SettingsInterface.KeyScanInterval, SettingsInterface.DefaultScanInterval.toString).toInt * 1000
mainHandler.postDelayed(new Runnable {
override def run(): Unit = discover()
}, scanInterval)
@@ -128,7 +128,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
override def onReceive(context: Context, intent: Intent): Unit = {
discovered.filterNot(d => connections.keySet.contains(d.id))
.foreach { d =>
- new ConnectThread(d, connectionOpened).start()
+ new BluetoothConnectThread(d, connectionOpened).start()
devices += (d.id -> d)
}
discovered = Set[Device]()
@@ -162,7 +162,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
def connectionOpened(device: Device, socket: BluetoothSocket): Unit = {
devices += (device.id -> device)
connections += (device.id ->
- new TransferThread(context, device, socket, this, crypto, onReceiveMessage))
+ new BluetoothTransferThread(context, device, socket, this, crypto, onReceiveMessage))
connections(device.id).start()
}
@@ -198,13 +198,18 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
/**
* Sends the message to nextHop.
*/
- override def send(nextHop: Address, msg: Message): Unit =
- connections.get(addressDeviceMap(nextHop)).foreach(_.send(msg))
+ override def send(nextHop: Address, msg: Message): Unit = {
+ addressDeviceMap
+ .find(_._1 == nextHop)
+ .map(i => connections.get(i._2))
+ .getOrElse(None)
+ .foreach(_.send(msg))
+ }
/**
* Returns all active Bluetooth connections.
*/
- def getConnections: Set[Address] =
+ override def getConnections: Set[Address] =
connections.map(x => addressDeviceMap.find(_._2 == x._1).get._1).toSet
}
diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala
similarity index 95%
rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala
index 7bc2734..accb172 100644
--- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala
@@ -10,7 +10,7 @@ import android.util.Log
*
* @param name Service name to broadcast.
*/
-class ListenThread(name: String, adapter: BluetoothAdapter,
+class BluetoothListenThread(name: String, adapter: BluetoothAdapter,
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
private val Tag = "ListenThread"
diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala
similarity index 95%
rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala
index e033743..406ec82 100644
--- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala
@@ -17,7 +17,7 @@ import com.nutomic.ensichat.core.{Address, Crypto, Message}
* @param socket An open socket to the given device.
* @param onReceive Called when a message was received from the other device.
*/
-class TransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface,
+class BluetoothTransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface,
crypto: Crypto, onReceive: (Message, Device.ID) => Unit) extends Thread {
private val Tag = "TransferThread"
@@ -30,6 +30,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
+ close()
null
}
@@ -39,6 +40,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
+ close()
null
}
diff --git a/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
index 09621e2..e375407 100644
--- a/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
@@ -15,7 +15,7 @@ import android.view._
import android.widget.{ListView, TextView}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
-import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.core.interfaces.SettingsInterface
import com.nutomic.ensichat.service.{CallbackHandler, ChatService}
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter
@@ -102,7 +102,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
bundle.putString(
IdenticonFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString)
bundle.putString(
- IdenticonFragment.ExtraUserName, prefs.getString(Settings.KeyUserName, ""))
+ IdenticonFragment.ExtraUserName, prefs.getString(SettingsInterface.KeyUserName, ""))
fragment.setArguments(bundle)
fragment.show(getFragmentManager, "dialog")
true
diff --git a/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
index 7c5aed4..724629a 100644
--- a/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
@@ -3,14 +3,13 @@ package com.nutomic.ensichat.fragments
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
-import android.preference.Preference.OnPreferenceChangeListener
-import android.preference.{Preference, PreferenceFragment, PreferenceManager}
-import com.nutomic.ensichat.{BuildConfig, R}
+import android.preference.{PreferenceFragment, PreferenceManager}
import com.nutomic.ensichat.activities.EnsichatActivity
import com.nutomic.ensichat.core.body.UserInfo
-import com.nutomic.ensichat.core.interfaces.Settings._
+import com.nutomic.ensichat.core.interfaces.SettingsInterface._
import com.nutomic.ensichat.fragments.SettingsFragment._
import com.nutomic.ensichat.util.Database
+import com.nutomic.ensichat.{BuildConfig, R}
object SettingsFragment {
val Version = "version"
diff --git a/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala b/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala
index 8b570d7..0301bd7 100644
--- a/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala
@@ -2,8 +2,8 @@ package com.nutomic.ensichat.service
import android.content.{Context, Intent}
import android.support.v4.content.LocalBroadcastManager
-import com.nutomic.ensichat.core.{ConnectionHandler, Message}
import com.nutomic.ensichat.core.interfaces.CallbackInterface
+import com.nutomic.ensichat.core.{ConnectionHandler, Message}
import com.nutomic.ensichat.service.CallbackHandler._
object CallbackHandler {
diff --git a/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala b/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala
index 92da36c..73d321c 100644
--- a/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala
@@ -6,9 +6,8 @@ import android.app.Service
import android.content.{Context, Intent}
import android.os.Handler
import com.nutomic.ensichat.bluetooth.BluetoothInterface
-import com.nutomic.ensichat.core.interfaces.Log
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
-import com.nutomic.ensichat.util.{Database, PRNGFixes, SettingsWrapper}
+import com.nutomic.ensichat.util.{Database, SettingsWrapper}
object ChatService {
@@ -17,6 +16,8 @@ object ChatService {
private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
+ val ActionNetworkChanged = "network_changed"
+
}
class ChatService extends Service {
@@ -29,23 +30,26 @@ class ChatService extends Service {
private lazy val connectionHandler =
new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
- ChatService.keyFolder(this))
+ ChatService.newCrypto(this))
override def onBind(intent: Intent) = binder
- override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
+ override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
+ if (intent.getAction == ChatService.ActionNetworkChanged)
+ connectionHandler.internetConnectionChanged()
+
+ Service.START_STICKY
+ }
/**
* Generates keys and starts Bluetooth interface.
*/
override def onCreate(): Unit = {
super.onCreate()
- PRNGFixes.apply()
- Log.setLogClass(classOf[android.util.Log])
notificationHandler.showPersistentNotification()
+ connectionHandler.addTransmissionInterface(new BluetoothInterface(this, new Handler(),
+ connectionHandler))
connectionHandler.start()
- connectionHandler.setTransmissionInterface(new BluetoothInterface(this, new Handler(),
- connectionHandler))
}
override def onDestroy(): Unit = {
diff --git a/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala b/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala
index 8210e06..ca8bf06 100644
--- a/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala
@@ -8,7 +8,7 @@ import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.MainActivity
import com.nutomic.ensichat.core.Message
import com.nutomic.ensichat.core.body.Text
-import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.core.interfaces.SettingsInterface
import com.nutomic.ensichat.service.NotificationHandler._
object NotificationHandler {
@@ -65,7 +65,7 @@ class NotificationHandler(context: Context) {
*/
private def defaults(): Int = {
val sp = PreferenceManager.getDefaultSharedPreferences(context)
- if (sp.getBoolean(Settings.KeyNotificationSoundsOn, Settings.DefaultNotificationSoundsOn))
+ if (sp.getBoolean(SettingsInterface.KeyNotificationSoundsOn, SettingsInterface.DefaultNotificationSoundsOn))
Notification.DEFAULT_ALL
else
Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
diff --git a/android/src/main/scala/com/nutomic/ensichat/util/Logging.scala b/android/src/main/scala/com/nutomic/ensichat/util/Logging.scala
new file mode 100644
index 0000000..e49cabe
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/util/Logging.scala
@@ -0,0 +1,13 @@
+package com.nutomic.ensichat.util
+
+import android.util
+import com.nutomic.ensichat.core.interfaces.Log
+
+class Logging extends Log {
+
+ def v(tag: String, message: String, tr: Throwable = null) = util.Log.v(tag, message, tr)
+ def d(tag: String, message: String, tr: Throwable = null) = util.Log.d(tag, message, tr)
+ def i(tag: String, message: String, tr: Throwable = null) = util.Log.i(tag, message, tr)
+ def w(tag: String, message: String, tr: Throwable = null) = util.Log.w(tag, message, tr)
+ def e(tag: String, message: String, tr: Throwable = null) = util.Log.e(tag, message, tr)
+}
\ No newline at end of file
diff --git a/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala b/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala
new file mode 100644
index 0000000..8760284
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala
@@ -0,0 +1,15 @@
+package com.nutomic.ensichat.util
+
+import android.content.{Intent, Context, BroadcastReceiver}
+import android.net.ConnectivityManager
+import com.nutomic.ensichat.service.ChatService
+
+class NetworkChangedReceiver extends BroadcastReceiver {
+
+ override def onReceive(context: Context, intent: Intent): Unit = {
+ val intent = new Intent(context, classOf[ChatService])
+ intent.setAction(ChatService.ActionNetworkChanged)
+ context.startService(intent)
+ }
+
+}
diff --git a/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala b/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala
index fc8bd8e..17c7ae8 100644
--- a/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala
@@ -2,9 +2,9 @@ package com.nutomic.ensichat.util
import android.content.Context
import android.preference.PreferenceManager
-import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.core.interfaces.SettingsInterface
-class SettingsWrapper(context: Context) extends Settings {
+class SettingsWrapper(context: Context) extends SettingsInterface {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/Address.scala b/core/src/main/scala/com/nutomic/ensichat/core/Address.scala
index 59ba756..cab8638 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/Address.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Address.scala
@@ -24,7 +24,7 @@ object Address {
*
* @param bytes SHA-256 hash of the node's public key.
*/
-case class Address(bytes: Array[Byte]) {
+final case class Address(bytes: Array[Byte]) {
require(bytes.length == Address.Length, "Invalid address length (was " + bytes.length + ")")
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala b/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala
index 90aa9f1..4332552 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala
@@ -6,6 +6,7 @@ import java.util.Date
import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo}
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces._
+import com.nutomic.ensichat.core.internet.InternetInterface
import com.nutomic.ensichat.core.util.FutureHelper
import scala.concurrent.ExecutionContext.Implicits.global
@@ -13,14 +14,12 @@ import scala.concurrent.ExecutionContext.Implicits.global
/**
* High-level handling of all message transfers and callbacks.
*/
-class ConnectionHandler(settings: Settings, database: DatabaseInterface,
- callbacks: CallbackInterface, keyFolder: File) {
+final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInterface,
+ callbacks: CallbackInterface, crypto: Crypto) {
private val Tag = "ConnectionHandler"
- private lazy val crypto = new Crypto(settings, keyFolder)
-
- private var transmissionInterface: TransmissionInterface = _
+ private var transmissionInterfaces = Set[TransmissionInterface]()
private lazy val router = new Router(connections, sendVia)
@@ -40,16 +39,20 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
FutureHelper {
crypto.generateLocalKeys()
Log.i(Tag, "Service started, address is " + crypto.localAddress)
+ transmissionInterfaces += new InternetInterface(this, crypto)
+ transmissionInterfaces.foreach(_.create())
}
}
def stop(): Unit = {
- transmissionInterface.destroy()
+ transmissionInterfaces.foreach(_.destroy())
}
- def setTransmissionInterface(interface: TransmissionInterface) = {
- transmissionInterface = interface
- transmissionInterface.create()
+ /**
+ * NOTE: This *must* be called before [[start()]], or it will have no effect.
+ */
+ def addTransmissionInterface(interface: TransmissionInterface) = {
+ transmissionInterfaces += interface
}
/**
@@ -70,7 +73,7 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
}
private def sendVia(nextHop: Address, msg: Message) =
- transmissionInterface.send(nextHop, msg)
+ transmissionInterfaces.foreach(_.send(nextHop, msg))
/**
* Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]].
@@ -78,10 +81,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
def onMessageReceived(msg: Message): Unit = {
if (msg.header.target == crypto.localAddress) {
crypto.verifyAndDecrypt(msg) match {
- case Some(msg) => onNewMessage(msg)
- case None =>
- Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
- return
+ case Some(m) => onNewMessage(m)
+ case None => Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
}
} else {
router.onReceive(msg)
@@ -118,7 +119,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
* @return True if the connection is valid
*/
def onConnectionOpened(msg: Message): Boolean = {
- val maxConnections = settings.get(Settings.KeyMaxConnections, Settings.DefaultMaxConnections.toString).toInt
+ val maxConnections = settings.get(SettingsInterface.KeyMaxConnections,
+ SettingsInterface.DefaultMaxConnections.toString).toInt
if (connections().size == maxConnections) {
Log.i(Tag, "Maximum number of connections reached")
return false
@@ -144,17 +146,22 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
}
Log.i(Tag, "Node " + sender + " connected")
- sendTo(sender, new UserInfo(settings.get(Settings.KeyUserName, ""),
- settings.get(Settings.KeyUserStatus, "")))
+ sendTo(sender, new UserInfo(settings.get(SettingsInterface.KeyUserName, ""),
+ settings.get(SettingsInterface.KeyUserStatus, "")))
callbacks.onConnectionsChanged()
true
}
def onConnectionClosed() = callbacks.onConnectionsChanged()
- def connections() = transmissionInterface.getConnections
+ def connections(): Set[Address] = transmissionInterfaces.flatMap(_.getConnections)
def getUser(address: Address) =
knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, ""))
+ def internetConnectionChanged(): Unit = {
+ transmissionInterfaces
+ .find(_.isInstanceOf[InternetInterface])
+ .foreach(_.asInstanceOf[InternetInterface].connectionChanged())
+ }
}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala b/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala
index 5e72d31..cc5cd81 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala
@@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
import com.nutomic.ensichat.core.Crypto._
import com.nutomic.ensichat.core.body._
import com.nutomic.ensichat.core.header.ContentHeader
-import com.nutomic.ensichat.core.interfaces.{Log, Settings}
+import com.nutomic.ensichat.core.interfaces.{Log, SettingsInterface}
object Crypto {
@@ -20,6 +20,11 @@ object Crypto {
*/
val PublicKeyAlgorithm = "RSA"
+ /**
+ * Algorithm used to read public keys.
+ */
+ val CipherAlgorithm = "RSA/ECB/PKCS1Padding"
+
/**
* Length of the local public/private keypair in bits.
*/
@@ -50,7 +55,7 @@ object Crypto {
/**
* Name of the preference where the local address is stored.
*/
- private val LocalAddressKey = "local_address"
+ val LocalAddressKey = "local_address"
/**
* Filename of the local private key in [[Crypto.keyFolder]].
@@ -60,7 +65,7 @@ object Crypto {
/**
* Filename of the local public key in [[Crypto.keyFolder]].
*/
- private val PublicKeyAlias = "local-public"
+ val PublicKeyAlias = "local-public"
}
@@ -69,17 +74,17 @@ object Crypto {
*
* @param keyFolder Folder where private and public keys are stored.
*/
-class Crypto(settings: Settings, keyFolder: File) {
+class Crypto(settings: SettingsInterface, keyFolder: File) {
private val Tag = "Crypto"
/**
- * Generates a new key pair using [[ [[Crypto.keyFolder]].]] with [[KeySize]] bits and stores the
+ * Generates a new key pair using [[keyFolder]] with [[PublicKeySize]] bits and stores the
* keys.
*
* Does nothing if the key pair already exists.
*/
- def generateLocalKeys(): Unit = {
+ private[core] def generateLocalKeys(): Unit = {
if (localKeysExist)
return
@@ -92,7 +97,7 @@ class Crypto(settings: Settings, keyFolder: File) {
address = calculateAddress(keyPair.getPublic)
- // The hash must have at least one bit set to not collide with the broadcast address.
+ // Never generate an invalid address.
} while(address == Address.Broadcast || address == Address.Null)
settings.put(LocalAddressKey, address.toString)
@@ -105,7 +110,7 @@ class Crypto(settings: Settings, keyFolder: File) {
/**
* Returns true if we have a public key stored for the given device.
*/
- def havePublicKey(address: Address): Boolean = new File(keyFolder, address.toString).exists()
+ private[core] def havePublicKey(address: Address) = new File(keyFolder, address.toString).exists()
/**
* Returns the public key for the given device.
@@ -113,7 +118,7 @@ class Crypto(settings: Settings, keyFolder: File) {
* @throws RuntimeException If the key does not exist.
*/
@throws[RuntimeException]
- def getPublicKey(address: Address): PublicKey = {
+ private[core] def getPublicKey(address: Address): PublicKey = {
loadKey(address.toString, classOf[PublicKey])
}
@@ -123,7 +128,7 @@ class Crypto(settings: Settings, keyFolder: File) {
* @throws RuntimeException If a key already exists for this address.
*/
@throws[RuntimeException]
- def addPublicKey(address: Address, key: PublicKey): Unit = {
+ private[core] def addPublicKey(address: Address, key: PublicKey): Unit = {
if (havePublicKey(address))
throw new RuntimeException("Already have key for " + address + ", not overwriting")
@@ -138,7 +143,7 @@ class Crypto(settings: Settings, keyFolder: File) {
new Message(msg.header, new CryptoData(Option(sig.sign), msg.crypto.key), msg.body)
}
- def verify(msg: Message, key: Option[PublicKey] = None): Boolean = {
+ private[core] def verify(msg: Message, key: Option[PublicKey] = None): Boolean = {
val sig = Signature.getInstance(SigningAlgorithm)
lazy val defaultKey = loadKey(msg.header.origin.toString, classOf[PublicKey])
sig.initVerify(key.getOrElse(defaultKey))
@@ -149,7 +154,7 @@ class Crypto(settings: Settings, keyFolder: File) {
/**
* Returns true if the local private and public key exist.
*/
- def localKeysExist = new File(keyFolder, PublicKeyAlias).exists()
+ private[core] def localKeysExist = new File(keyFolder, PublicKeyAlias).exists()
/**
* Returns the local public key.
@@ -193,7 +198,7 @@ class Crypto(settings: Settings, keyFolder: File) {
* @return The key read from storage.
* @throws RuntimeException If the key does not exist.
*/
- private def loadKey[T](alias: String, keyType: Class[T]): T = {
+ private[core] def loadKey[T](alias: String, keyType: Class[T]): T = {
val path = new File(keyFolder, alias)
if (!path.exists()) {
throw new RuntimeException("The requested key with alias " + alias + " does not exist")
@@ -221,11 +226,11 @@ class Crypto(settings: Settings, keyFolder: File) {
}
}
- def encryptAndSign(msg: Message, key: Option[PublicKey] = None): Message = {
+ private[core] def encryptAndSign(msg: Message, key: Option[PublicKey] = None): Message = {
sign(encrypt(msg, key))
}
- def verifyAndDecrypt(msg: Message, key: Option[PublicKey] = None): Option[Message] = {
+ private[core] def verifyAndDecrypt(msg: Message, key: Option[PublicKey] = None): Option[Message] = {
if (verify(msg, key))
Option(decrypt(msg))
else
@@ -240,7 +245,7 @@ class Crypto(settings: Settings, keyFolder: File) {
val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.body.write))
// Asymmetric encryption of secret key
- val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm)
+ val asymmetricCipher = Cipher.getInstance(CipherAlgorithm)
lazy val defaultKey = loadKey(msg.header.target.toString, classOf[PublicKey])
asymmetricCipher.init(Cipher.WRAP_MODE, key.getOrElse(defaultKey))
@@ -250,7 +255,7 @@ class Crypto(settings: Settings, keyFolder: File) {
private def decrypt(msg: Message): Message = {
// Asymmetric decryption of secret key
- val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm)
+ val asymmetricCipher = Cipher.getInstance(CipherAlgorithm)
asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey]))
val key = asymmetricCipher.unwrap(msg.crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY)
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/Router.scala b/core/src/main/scala/com/nutomic/ensichat/core/Router.scala
index d138922..9f6415d 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/Router.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Router.scala
@@ -5,7 +5,7 @@ import com.nutomic.ensichat.core.header.{ContentHeader, MessageHeader}
/**
* Forwards messages to all connected devices.
*/
-class Router(activeConnections: () => Set[Address], send: (Address, Message) => Unit) {
+final private[core] class Router(activeConnections: () => Set[Address], send: (Address, Message) => Unit) {
private var messageSeen = Set[(Address, Int)]()
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala b/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala
index 3674602..ca39c49 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala
@@ -1,12 +1,12 @@
package com.nutomic.ensichat.core
import com.nutomic.ensichat.core.header.ContentHeader
-import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.core.interfaces.SettingsInterface
/**
* Generates sequence numbers according to protocol, which are stored persistently.
*/
-class SeqNumGenerator(preferences: Settings) {
+final private[core] class SeqNumGenerator(preferences: SettingsInterface) {
private val KeySequenceNumber = "sequence_number"
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/User.scala b/core/src/main/scala/com/nutomic/ensichat/core/User.scala
index e82e073..412181c 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/User.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/User.scala
@@ -1,3 +1,3 @@
package com.nutomic.ensichat.core
-case class User(address: Address, name: String, status: String)
+final case class User(address: Address, name: String, status: String)
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala
index 550e2c2..092df0c 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala
@@ -32,7 +32,7 @@ object ConnectionInfo {
/**
* Holds a node's public key.
*/
-case class ConnectionInfo(key: PublicKey) extends MessageBody {
+final case class ConnectionInfo(key: PublicKey) extends MessageBody {
override def protocolType = ConnectionInfo.Type
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala
index f0fdbe1..bd3a55b 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala
@@ -35,7 +35,7 @@ object CryptoData {
/**
* Holds the signature and (optional) key that are stored in a message.
*/
-case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) {
+final case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) {
override def equals(a: Any): Boolean = a match {
case o: CryptoData => util.Arrays.equals(signature.orNull, o.signature.orNull) &&
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala
index 2447cbb..5c6c18d 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala
@@ -3,7 +3,7 @@ package com.nutomic.ensichat.core.body
/**
* Represents the data in an encrypted message body.
*/
-case class EncryptedBody(data: Array[Byte]) extends MessageBody {
+final case class EncryptedBody(data: Array[Byte]) extends MessageBody {
override def protocolType = -1
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala
index eda34d1..d99bff6 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala
@@ -25,7 +25,7 @@ object Text {
/**
* Holds a plain text message.
*/
-case class Text(text: String) extends MessageBody {
+final case class Text(text: String) extends MessageBody {
override def protocolType = -1
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala
index d8c9cbc..73a1fdb 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala
@@ -29,7 +29,7 @@ object UserInfo {
/**
* Holds display name and status of the sender.
*/
-case class UserInfo(name: String, status: String) extends MessageBody {
+final case class UserInfo(name: String, status: String) extends MessageBody {
override def protocolType = -1
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala
index 1eed421..dd9c7a7 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala
@@ -39,7 +39,7 @@ object ContentHeader {
*
* This is [[AbstractHeader]] with messageId and time fields set.
*/
-case class ContentHeader(override val origin: Address,
+final case class ContentHeader(override val origin: Address,
override val target: Address,
override val seqNum: Int,
contentType: Int,
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala
index 90c7b78..c5060b5 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala
@@ -41,7 +41,7 @@ object MessageHeader {
*
* This is the same as [[AbstractHeader]].
*/
-case class MessageHeader(override val protocolType: Int,
+final case class MessageHeader(override val protocolType: Int,
override val origin: Address,
override val target: Address,
override val seqNum: Int,
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala
index e828441..e06f316 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala
@@ -7,4 +7,5 @@ trait CallbackInterface {
def onMessageReceived(msg: Message): Unit
def onConnectionsChanged(): Unit
+
}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala
index c565b43..d9a3c47 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala
@@ -2,28 +2,24 @@ package com.nutomic.ensichat.core.interfaces
object Log {
- def setLogClass[T](logClass: Class[T]) = {
- this.logClass = Option(logClass)
- }
+ def setLogInstance(log: Log) = instance = Option(log)
- private var logClass: Option[Class[_]] = None
+ private var instance: Option[Log] = None
- def v(tag: String, message: String, tr: Throwable = null) = log("v", tag, message, tr)
-
- def d(tag: String, message: String, tr: Throwable = null) = log("d", tag, message, tr)
-
- def i(tag: String, message: String, tr: Throwable = null) = log("i", tag, message, tr)
-
- def w(tag: String, message: String, tr: Throwable = null) = log("w", tag, message, tr)
-
- def e(tag: String, message: String, tr: Throwable = null) = log("e", tag, message, tr)
-
- private def log(level: String, tag: String, message: String, throwable: Throwable) = logClass match {
- case Some(l) =>
- l.getMethod(level, classOf[String], classOf[String], classOf[Throwable])
- .invoke(null, tag, message, throwable)
- case None =>
- System.out.println(level + tag + message + throwable)
- }
+ def v(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.v(tag, message, tr))
+ def d(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.d(tag, message, tr))
+ def i(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.i(tag, message, tr))
+ def w(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.w(tag, message, tr))
+ def e(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.e(tag, message, tr))
+
+}
+
+trait Log {
+
+ def v(tag: String, message: String, tr: Throwable = null)
+ def d(tag: String, message: String, tr: Throwable = null)
+ def i(tag: String, message: String, tr: Throwable = null)
+ def w(tag: String, message: String, tr: Throwable = null)
+ def e(tag: String, message: String, tr: Throwable = null)
}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala
similarity index 87%
rename from core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala
index 93c0824..1965f85 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala
@@ -1,6 +1,6 @@
package com.nutomic.ensichat.core.interfaces
-object Settings {
+object SettingsInterface {
val KeyUserName = "user_name"
val KeyUserStatus = "user_status"
@@ -26,9 +26,9 @@ object Settings {
/**
* Interface for persistent storage of key value pairs.
*
- * Must support at least storage of strings and integers.
+ * Must support at least storage of String, Int, Long.
*/
-trait Settings {
+trait SettingsInterface {
def put[T](key: String, value: T): Unit
def get[T](key: String, default: T): T
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala
new file mode 100644
index 0000000..be911cf
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala
@@ -0,0 +1,87 @@
+package com.nutomic.ensichat.core.internet
+
+import java.io.{IOException, InputStream, OutputStream}
+import java.net.{InetAddress, Socket}
+
+import com.nutomic.ensichat.core.Message.ReadMessageException
+import com.nutomic.ensichat.core.body.ConnectionInfo
+import com.nutomic.ensichat.core.header.MessageHeader
+import com.nutomic.ensichat.core.interfaces.Log
+import com.nutomic.ensichat.core.{Address, Crypto, Message}
+
+/**
+ * Encapsulates an active connection to another node.
+ */
+class InternetConnectionThread(socket: Socket, crypto: Crypto, onDisconnected: (InternetConnectionThread) => Unit,
+ onReceive: (Message, InternetConnectionThread) => Unit) extends Thread {
+
+ private val Tag = "InternetConnectionThread"
+
+ private val inStream: InputStream =
+ try {
+ socket.getInputStream
+ } catch {
+ case e: IOException =>
+ Log.e(Tag, "Failed to open stream", e)
+ close()
+ null
+ }
+
+ private val outStream: OutputStream =
+ try {
+ socket.getOutputStream
+ } catch {
+ case e: IOException =>
+ Log.e(Tag, "Failed to open stream", e)
+ close()
+ null
+ }
+
+ def internetAddress(): InetAddress = {
+ socket.getInetAddress
+ }
+
+ override def run(): Unit = {
+ Log.i(Tag, "Connection opened to " + socket.getInetAddress)
+
+ send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
+ Address.Null, Address.Null, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
+
+ socket.setKeepAlive(true)
+ while (socket.isConnected) {
+ try {
+ if (inStream.available() > 0) {
+ val msg = Message.read(inStream)
+
+ onReceive(msg, this)
+ }
+ } catch {
+ case e @ (_: ReadMessageException | _: IOException) =>
+ Log.w(Tag, "Failed to read incoming message", e)
+ close()
+ return
+ }
+ }
+ close()
+ Log.d(Tag, "exited: " + socket.isConnected)
+ }
+
+ def send(msg: Message): Unit = {
+ try {
+ outStream.write(msg.write)
+ } catch {
+ case e: IOException => Log.e(Tag, "Failed to write message", e)
+ }
+ }
+
+ def close(): Unit = {
+ try {
+ socket.close()
+ } catch {
+ case e: IOException => Log.w(Tag, "Failed to close socket", e)
+ }
+ Log.d(Tag, "Connection to " + socket.getInetAddress + " closed")
+ onDisconnected(this)
+ }
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala
new file mode 100644
index 0000000..9a36795
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala
@@ -0,0 +1,129 @@
+package com.nutomic.ensichat.core.internet
+
+import java.io.IOException
+import java.net.{InetAddress, Socket}
+
+import com.nutomic.ensichat.core.body.ConnectionInfo
+import com.nutomic.ensichat.core.interfaces.{Log, TransmissionInterface}
+import com.nutomic.ensichat.core.util.FutureHelper
+import com.nutomic.ensichat.core.{Address, ConnectionHandler, Crypto, Message}
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object InternetInterface {
+
+ val Port = 26344
+
+ val BootstrapNodes = Set("192.168.1.104:26344", // T420
+ "46.101.229.4:26344") // digital ocean
+
+}
+
+/**
+ * Handles all Internet connectivity.
+ *
+ * TODO: send message through internet node: android -> server -> android
+ * TODO: send test reply to incoming messages
+ * TODO: send a list of all connected nodes when we connect to a node
+ */
+class InternetInterface(connectionHandler: ConnectionHandler, crypto: Crypto)
+ extends TransmissionInterface {
+
+ private val Tag = "InternetInterface"
+
+ private lazy val serverThread = new InternetServerThread(crypto, onConnected, onDisconnected, onReceiveMessage)
+
+ private var connections = Set[InternetConnectionThread]()
+
+ private var addressDeviceMap = Map[Address, InternetConnectionThread]()
+
+ /**
+ * Initializes and starts discovery and listening.
+ */
+ override def create(): Unit = {
+ FutureHelper {
+ serverThread.start()
+ InternetInterface.BootstrapNodes.foreach(openConnection)
+ }
+ }
+
+ /**
+ * Stops discovery and listening.
+ */
+ override def destroy(): Unit = {
+ serverThread.cancel()
+ connections.foreach(_.close())
+ }
+
+ private def openConnection(addressPort: String): Unit = {
+ val split = addressPort.split(":")
+ openConnection(split(0), split(1).toInt)
+ }
+
+ /**
+ * Opens connection to the specified IP address in client mode.
+ */
+ private def openConnection(nodeAddress: String, port: Int): Unit = {
+ try {
+ val socket = new Socket(InetAddress.getByName(nodeAddress), port)
+ val ct = new InternetConnectionThread(socket, crypto, onDisconnected, onReceiveMessage)
+ connections += ct
+ ct.start()
+ } catch {
+ case e: IOException =>
+ Log.w(Tag, "Failed to open connection to " + nodeAddress + ":" + port, e)
+ }
+ }
+
+ private def onConnected(connectionThread: InternetConnectionThread): Unit = {
+ connections += connectionThread
+ }
+
+ private def onDisconnected(connectionThread: InternetConnectionThread): Unit = {
+ addressDeviceMap.find(_._2 == connectionThread).foreach { ad =>
+ Log.d(Tag, "Connection closed to " + ad._1)
+ connections -= connectionThread
+ addressDeviceMap -= ad._1
+ connectionHandler.onConnectionClosed()
+ }
+ }
+
+ private def onReceiveMessage(msg: Message, thread: InternetConnectionThread): Unit = msg.body match {
+ case info: ConnectionInfo =>
+ val address = crypto.calculateAddress(info.key)
+ if (address == crypto.localAddress) {
+ Log.i(Tag, "Address " + address + " is me, not connecting to myself")
+ thread.close()
+ return
+ }
+
+ // Service.onConnectionOpened sends message, so mapping already needs to be in place.
+ addressDeviceMap += (address -> thread)
+ if (!connectionHandler.onConnectionOpened(msg))
+ addressDeviceMap -= address
+ case _ =>
+ connectionHandler.onMessageReceived(msg)
+ }
+
+ /**
+ * Sends the message to nextHop.
+ */
+ override def send(nextHop: Address, msg: Message): Unit = {
+ addressDeviceMap
+ .find(_._1 == nextHop)
+ .foreach(_._2.send(msg))
+ }
+
+ /**
+ * Returns all active Internet connections.
+ */
+ override def getConnections = addressDeviceMap.keySet
+
+ def connectionChanged(): Unit = {
+ FutureHelper {
+ Log.i(Tag, "Network has changed. Close all connections and connect to bootstrap nodes again")
+ connections.foreach(_.close())
+ InternetInterface.BootstrapNodes.foreach(openConnection)
+ }
+ }
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala
new file mode 100644
index 0000000..d296a84
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala
@@ -0,0 +1,44 @@
+package com.nutomic.ensichat.core.internet
+
+import java.io.{IOException, PrintStream}
+import java.net.{Socket, ServerSocket}
+
+import com.nutomic.ensichat.core.{Message, Crypto}
+import com.nutomic.ensichat.core.interfaces.Log
+
+import scala.io.BufferedSource
+
+class InternetServerThread(crypto: Crypto, onConnected: (InternetConnectionThread) => Unit,
+ onDisconnected: (InternetConnectionThread) => Unit, onReceive: (Message, InternetConnectionThread) => Unit) extends Thread {
+
+ private val Tag = "InternetServerThread"
+
+ private lazy val socket: Option[ServerSocket] = try {
+ Option(new ServerSocket(InternetInterface.Port))
+ } catch {
+ case e: IOException =>
+ Log.w(Tag, "Failed to create server socket", e)
+ None
+ }
+
+ override def run(): Unit = {
+ try {
+ while (socket.get.isBound) {
+ val connection = new InternetConnectionThread(socket.get.accept(), crypto, onDisconnected, onReceive)
+ onConnected(connection)
+ connection.start()
+ }
+ } catch {
+ case e: IOException => Log.w(Tag, "Failed to accept connection", e)
+ }
+ }
+
+ def cancel(): Unit = {
+ try {
+ socket.get.close()
+ } catch {
+ case e: IOException => Log.w(Tag, "Failed to close socket", e)
+ }
+ }
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala b/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala
index 55ae730..4dba573 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala
@@ -5,7 +5,7 @@ import java.nio.ByteBuffer
/**
* Provides various helper methods for [[ByteBuffer]].
*/
-object BufferUtils {
+private[core] object BufferUtils {
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala b/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala
index 17fd04a..76b3702 100644
--- a/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala
@@ -21,7 +21,7 @@ object FutureHelper {
// cross-platform way to execute on the foreground thread.
// We use this to make sure exceptions are not hidden in the logs.
Log.e(Tag, "Exception in Future", e)
- System.exit(-1)
+ //System.exit(-1)
}
f
}
diff --git a/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala
index 7bad8be..d08a215 100644
--- a/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala
@@ -2,13 +2,13 @@ package com.nutomic.ensichat.core
import java.io.File
-import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.core.interfaces.SettingsInterface
import junit.framework.TestCase
import org.junit.Assert._
object CryptoTest {
- class TestSettings extends Settings {
+ class TestSettings extends SettingsInterface {
private var map = Map[String, Any]()
override def get[T](key: String, default: T): T = map.getOrElse(key, default).asInstanceOf[T]
override def put[T](key: String, value: T): Unit = map += (key -> value)
diff --git a/gradle.properties b/gradle.properties
index 5d08ba7..5f6cf19 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,4 +15,7 @@
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
\ No newline at end of file
+# org.gradle.parallel=true
+
+versionName=0.1.7
+versionCode=8
diff --git a/server/.gitignore b/server/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/server/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/server/build.gradle b/server/build.gradle
new file mode 100644
index 0000000..43810ec
--- /dev/null
+++ b/server/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'scala'
+apply plugin: 'application'
+
+dependencies {
+ compile 'org.scala-lang:scala-library:2.11.7'
+ compile project(path: ':core')
+ compile 'com.github.scopt:scopt_2.10:3.3.0'
+}
+
+mainClassName = 'com.nutomic.ensichat.server.Main'
+version = properties.versionName
+applicationName = 'ensichat'
+
+run {
+ // Use this to pass command line arguments via `gradle run`.
+ // Example:
+ // ```
+ // ./gradlew server:run -Pargs="--help"
+ // ```
+ if (project.hasProperty('args')) {
+ args project.args.split('\\s+')
+ }
+}
diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Config.scala b/server/src/main/scala/com/nutomic/ensichat/server/Config.scala
new file mode 100644
index 0000000..8de29d6
--- /dev/null
+++ b/server/src/main/scala/com/nutomic/ensichat/server/Config.scala
@@ -0,0 +1,3 @@
+package com.nutomic.ensichat.server
+
+case class Config(name: Option[String] = None)
diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Database.scala b/server/src/main/scala/com/nutomic/ensichat/server/Database.scala
new file mode 100644
index 0000000..dc23fa7
--- /dev/null
+++ b/server/src/main/scala/com/nutomic/ensichat/server/Database.scala
@@ -0,0 +1,23 @@
+package com.nutomic.ensichat.server
+
+import com.nutomic.ensichat.core.interfaces.DatabaseInterface
+import com.nutomic.ensichat.core.{Address, Message, User}
+
+class Database extends DatabaseInterface {
+
+ private var contacts = Set[User]()
+
+ def onMessageReceived(msg: Message): Unit = {}
+
+ def getContacts: Set[User] = contacts
+
+ def getContact(address: Address): Option[User] = contacts.find(_.address == address)
+
+ def addContact(contact: User): Unit = contacts += contact
+
+ def updateContact(contact: User): Unit = {
+ contacts = contacts.filterNot(_.address == contact.address)
+ contacts += contact
+ }
+
+}
diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Logging.scala b/server/src/main/scala/com/nutomic/ensichat/server/Logging.scala
new file mode 100644
index 0000000..e384c0e
--- /dev/null
+++ b/server/src/main/scala/com/nutomic/ensichat/server/Logging.scala
@@ -0,0 +1,42 @@
+package com.nutomic.ensichat.server
+
+import java.io.{PrintWriter, StringWriter}
+import java.text.DateFormat
+import java.util.{Date, Locale}
+
+import com.nutomic.ensichat.core.interfaces.Log
+
+import scala.collection.mutable
+
+class Logging extends Log {
+
+ private val logs = new mutable.Queue[String]()
+
+ def dequeue(): Seq[String] = logs.dequeueAll((String) => true)
+
+ private def enqueue(tag: String, message: String, tr: Option[Throwable]): Unit = {
+ val df = DateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.UK)
+ val throwableString = tr.map { tr =>
+ val sw = new StringWriter()
+ tr.printStackTrace(new PrintWriter(sw))
+ "\n" + sw.toString
+ }
+ logs.enqueue(df.format(new Date()) + " " + tag + ": " + message + throwableString.getOrElse(""))
+ }
+
+ def v(tag: String, message: String, tr: Throwable = null): Unit =
+ enqueue("V/" + tag, message, Option(tr))
+
+ def d(tag: String, message: String, tr: Throwable = null): Unit =
+ enqueue("D/" + tag, message, Option(tr))
+
+ def i(tag: String, message: String, tr: Throwable = null): Unit =
+ enqueue("I/" + tag, message, Option(tr))
+
+ def w(tag: String, message: String, tr: Throwable = null): Unit =
+ enqueue("W/" + tag, message, Option(tr))
+
+ def e(tag: String, message: String, tr: Throwable = null): Unit =
+ enqueue("E/" + tag, message, Option(tr))
+
+}
\ No newline at end of file
diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Main.scala b/server/src/main/scala/com/nutomic/ensichat/server/Main.scala
new file mode 100644
index 0000000..96c9fe9
--- /dev/null
+++ b/server/src/main/scala/com/nutomic/ensichat/server/Main.scala
@@ -0,0 +1,74 @@
+package com.nutomic.ensichat.server
+
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+import com.nutomic.ensichat.core.body.Text
+import com.nutomic.ensichat.core.interfaces.{CallbackInterface, Log, SettingsInterface}
+import com.nutomic.ensichat.core.{Message, ConnectionHandler, Crypto}
+import scopt.OptionParser
+
+object Main extends App with CallbackInterface {
+
+ private val Tag = "Main"
+
+ private val ConfigFolder = new File(System.getProperty("user.home"), ".config/ensichat")
+ private val ConfigFile = new File(ConfigFolder, "config.properties")
+ private val KeyFolder = new File(ConfigFolder, "keys")
+
+ private val LogInterval = TimeUnit.SECONDS.toMillis(1)
+
+ private lazy val logInstance = new Logging()
+ private lazy val settings = new Settings(ConfigFile)
+ private lazy val crypto = new Crypto(settings, KeyFolder)
+ private lazy val connectionHandler = new ConnectionHandler(settings, new Database(), this, crypto)
+
+ init()
+
+ private def init(): Unit = {
+ ConfigFolder.mkdirs()
+ KeyFolder.mkdirs()
+ Log.setLogInstance(logInstance)
+ sys.addShutdownHook(connectionHandler.stop())
+ val parser = new OptionParser[Config]("ensichat") {
+ head("ensichat")
+ opt[String]('n', "name") action { (x, c) =>
+ c.copy(name = Option(x))
+ } text "the username for this node (optional)"
+ help("help") text "prints this usage text"
+ }
+
+ parser.parse(args, Config()).foreach { config =>
+ config.name.foreach(settings.put(SettingsInterface.KeyUserName, _))
+ run()
+ }
+ }
+
+ private def run(): Unit = {
+ connectionHandler.start()
+
+ // Keep alive and print logs
+ while (true) {
+ Thread.sleep(LogInterval)
+ logInstance.dequeue().foreach(System.out.println)
+ }
+ }
+
+ def onMessageReceived(msg: Message): Unit = {
+ if (msg.header.target != crypto.localAddress)
+ return
+
+ msg.body match {
+ case text: Text =>
+ val address = msg.header.origin
+ val name = connectionHandler.getUser(address).name
+ connectionHandler.sendTo(address, new Text("Hello " + name))
+ Log.i(Tag, "Received text: " + text.text)
+ case _ =>
+ Log.i(Tag, "Received msg: " + msg.body)
+ }
+ }
+
+ def onConnectionsChanged(): Unit = {}
+
+}
diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala b/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala
new file mode 100644
index 0000000..39473b3
--- /dev/null
+++ b/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala
@@ -0,0 +1,53 @@
+package com.nutomic.ensichat.server
+
+import java.io._
+import java.util.Properties
+
+import com.nutomic.ensichat.core.interfaces.{Log, SettingsInterface}
+
+import scala.collection.JavaConverters._
+
+class Settings(file: File) extends SettingsInterface {
+
+ private val Tag = "Settings"
+
+ if (!file.exists()) {
+ file.createNewFile()
+ put(SettingsInterface.KeyUserName, "unknown user")
+ }
+
+ private lazy val props: Properties = {
+ val p = new Properties()
+ try {
+ val fis = new InputStreamReader(new FileInputStream(file), "UTF-8")
+ p.load(fis)
+ fis.close()
+ } catch {
+ case e: IOException => Log.w(Tag, "Failed to load settings from " + file, e)
+ }
+ p
+ }
+
+ def put[T](key: String, value: T): Unit = {
+ props.asScala.put(key, value.toString)
+ try {
+ val fos = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")
+ props.store(fos, "")
+ fos.close()
+ } catch {
+ case e: IOException => Log.w(Tag, "Failed to write preference for key " + key, e)
+ }
+ }
+
+ def get[T](key: String, default: T): T = {
+ val value = props.asScala.getOrElse[String](key, default.toString)
+ val cast = default match {
+ case _: Int => value.toInt
+ case _: Long => value.toLong
+ case _: String => value
+ }
+ // This has no effect due to type erasure, but is needed to avoid compiler error.
+ cast.asInstanceOf[T]
+ }
+
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 00e5526..65899c0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':android', ':core'
+include ':android', ':core', ':server'