diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c7783cd..7f6f5fa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,7 +42,7 @@
android:value=".activities.MainActivity" />
-
+
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
index 159085d..56adb42 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
@@ -10,10 +10,8 @@ import android.view._
import android.widget.AdapterView.OnItemClickListener
import android.widget._
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.bluetooth.ChatService
-import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
-import com.nutomic.ensichat.protocol.{Address, Crypto}
+import com.nutomic.ensichat.protocol.{Address, ChatService, Crypto}
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
import scala.collection.SortedSet
@@ -23,14 +21,14 @@ import scala.collection.SortedSet
*
* Adding a contact requires confirmation on both sides.
*/
-class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyContactsChangedListener
- with OnItemClickListener with OnMessageReceivedListener {
+class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnectionsChangedListener
+ with OnItemClickListener with ChatService.OnMessageReceivedListener {
private val Tag = "AddContactsActivity"
private lazy val Adapter = new DevicesAdapter(this)
- private lazy val Database = service.database
+ private lazy val Database = service.Database
private lazy val Crypto = new Crypto(this)
@@ -46,8 +44,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
* @param localConfirmed If true, the local user has accepted adding the contact.
* @param remoteConfirmed If true, the remote contact has accepted adding this device as contact.
*/
- private class AddContactInfo(val localConfirmed: Boolean, val remoteConfirmed: Boolean) {
- }
+ private class AddContactInfo(val localConfirmed: Boolean, val remoteConfirmed: Boolean)
/**
* Initializes layout, registers connection and message listeners.
@@ -71,7 +68,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
/**
* Displays newly connected devices in the list.
*/
- override def onNearbyContactsChanged(devices: Set[Address]): Unit = {
+ override def onConnectionsChanged(devices: Set[Address]): Unit = {
runOnUiThread(new Runnable {
override def run(): Unit = {
Adapter.clear()
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/EnsiChatActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/EnsiChatActivity.scala
index 499051f..867192f 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/EnsiChatActivity.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/activities/EnsiChatActivity.scala
@@ -3,7 +3,8 @@ package com.nutomic.ensichat.activities
import android.app.Activity
import android.content.{ComponentName, Context, Intent, ServiceConnection}
import android.os.{Bundle, IBinder}
-import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder}
+import com.nutomic.ensichat.protocol.ChatService
+import com.nutomic.ensichat.protocol.ChatServiceBinder
/**
* Connects to [[ChatService]] and provides access to it.
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
new file mode 100644
index 0000000..9763477
--- /dev/null
+++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
@@ -0,0 +1,198 @@
+package com.nutomic.ensichat.bluetooth
+
+import java.util.UUID
+
+import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
+import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
+import android.preference.PreferenceManager
+import android.util.Log
+import com.google.common.collect.HashBiMap
+import com.nutomic.ensichat.R
+import com.nutomic.ensichat.protocol.ChatService.InterfaceHandler
+import com.nutomic.ensichat.protocol._
+import com.nutomic.ensichat.protocol.messages.{ConnectionInfo, Message}
+
+import scala.collection.immutable.HashMap
+
+object BluetoothInterface {
+
+ /**
+ * Bluetooth service UUID version 5, created with namespace URL and "ensichat.nutomic.com".
+ */
+ val AppUuid: UUID = UUID.fromString("8ed52b7a-4501-5348-b054-3d94d004656e")
+
+}
+
+/**
+ * Handles all Bluetooth connectivity.
+ */
+class BluetoothInterface(Service: ChatService, Crypto: Crypto) extends InterfaceHandler {
+
+ private val Tag = "BluetoothInterface"
+
+ private lazy val BtAdapter = BluetoothAdapter.getDefaultAdapter
+
+ private var devices = new HashMap[Device.ID, Device]()
+
+ private var connections = new HashMap[Device.ID, TransferThread]()
+
+ private lazy val ListenThread =
+ new ListenThread(Service.getString(R.string.app_name), BtAdapter, onConnectionOpened)
+
+ private var cancelDiscovery = false
+
+ private var discovered = Set[Device]()
+
+ private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]()
+
+ /**
+ * Initializes and starts discovery and listening.
+ */
+ override def create(): Unit = {
+ Service.registerReceiver(DeviceDiscoveredReceiver,
+ new IntentFilter(BluetoothDevice.ACTION_FOUND))
+ Service.registerReceiver(BluetoothStateReceiver,
+ new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
+ Service.registerReceiver(DiscoveryFinishedReceiver,
+ new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
+ startBluetoothConnections()
+ }
+
+ /**
+ * Stops discovery and listening.
+ */
+ override def destroy(): Unit = {
+ ListenThread.cancel()
+ cancelDiscovery = true
+ Service.unregisterReceiver(DeviceDiscoveredReceiver)
+ Service.unregisterReceiver(BluetoothStateReceiver)
+ Service.unregisterReceiver(DiscoveryFinishedReceiver)
+ }
+
+ /**
+ * Starts discovery and listening.
+ */
+ private def startBluetoothConnections(): Unit = {
+ ListenThread.start()
+ cancelDiscovery = false
+ discover()
+ }
+
+ /**
+ * Runs discovery as long as [[cancelDiscovery]] is false.
+ */
+ def discover(): Unit = {
+ if (cancelDiscovery)
+ return
+
+ if (!BtAdapter.isDiscovering) {
+ Log.v(Tag, "Starting discovery")
+ BtAdapter.startDiscovery()
+ }
+
+ val scanInterval = PreferenceManager.getDefaultSharedPreferences(Service)
+ .getString("scan_interval_seconds", "15").toInt * 1000
+ Service.MainHandler.postDelayed(new Runnable {
+ override def run(): Unit = discover()
+ }, scanInterval)
+ }
+
+ /**
+ * Stores newly discovered devices.
+ */
+ private val DeviceDiscoveredReceiver = new BroadcastReceiver() {
+ override def onReceive(context: Context, intent: Intent) {
+ discovered += new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
+ }
+ }
+
+ /**
+ * Initiates connection to discovered devices.
+ */
+ private val DiscoveryFinishedReceiver = new BroadcastReceiver() {
+ override def onReceive(context: Context, intent: Intent): Unit = {
+ discovered.filterNot(d => connections.keySet.contains(d.Id))
+ .foreach { d =>
+ new ConnectThread(d, onConnectionOpened).start()
+ devices += (d.Id -> d)
+ }
+ discovered = Set[Device]()
+ }
+ }
+
+ /**
+ * Starts or stops listening and discovery based on bluetooth state.
+ */
+ private val BluetoothStateReceiver = new BroadcastReceiver {
+ override def onReceive(context: Context, intent: Intent): Unit = {
+ intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) match {
+ case BluetoothAdapter.STATE_ON =>
+ if (Crypto.localKeysExist)
+ startBluetoothConnections()
+ case BluetoothAdapter.STATE_TURNING_OFF =>
+ Log.i(Tag, "Bluetooth disabled, stopping connectivity")
+ ListenThread.cancel()
+ cancelDiscovery = true
+ connections.foreach(_._2.close())
+ case _ =>
+ }
+ }
+ }
+
+
+ /**
+ * Initiates data transfer with device.
+ */
+ def onConnectionOpened(device: Device, socket: BluetoothSocket): Unit = {
+ devices += (device.Id -> device)
+ connections += (device.Id ->
+ new TransferThread(device, socket, this, Crypto, onReceiveMessage))
+ connections(device.Id).start()
+ }
+
+ /**
+ * Removes device from active connections.
+ */
+ def onConnectionClosed(device: Device, socket: BluetoothSocket): Unit = {
+ devices -= device.Id
+ connections -= device.Id
+ val inv = AddressDeviceMap.inverse()
+ Service.callConnectionListeners()
+ inv.remove(device.Id)
+ }
+
+ /**
+ * Passes incoming messages to [[ChatService]].
+ *
+ * Also uses [[ConnectionInfo]] message to determine mapping from [[Device.ID]] to [[Address]].
+ *
+ * @param msg The message that was received.
+ * @param device Device that sent the message.
+ */
+ private def onReceiveMessage(msg: Message, device: Device.ID): Unit = msg.Body match {
+ case info: ConnectionInfo =>
+ val sender = Service.onConnectionOpened(msg)
+ sender match {
+ case Some(s) =>
+ AddressDeviceMap.put(Crypto.calculateAddress(info.key), device)
+ Service.callConnectionListeners()
+ case None =>
+ }
+ case _ =>
+ Service.onMessageReceived(msg)
+ }
+
+ /**
+ * Sends the message to the target address specified in the message header.
+ */
+ override def send(msg: Message): Unit = {
+ connections.apply(AddressDeviceMap.get(msg.Header.Target)).send(msg)
+ }
+
+ /**
+ * Returns all active Bluetooth connections.
+ */
+ def getConnections: Set[Address] =
+ connections.map(x => AddressDeviceMap.inverse().get(x._1)).toSet
+
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala
deleted file mode 100644
index 28022ce..0000000
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala
+++ /dev/null
@@ -1,333 +0,0 @@
-package com.nutomic.ensichat.bluetooth
-
-import java.util.UUID
-
-import android.app.Service
-import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
-import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
-import android.os.Handler
-import android.preference.PreferenceManager
-import android.util.Log
-import com.google.common.collect.HashBiMap
-import com.nutomic.ensichat.R
-import com.nutomic.ensichat.bluetooth.ChatService.{OnMessageReceivedListener, OnNearbyContactsChangedListener}
-import com.nutomic.ensichat.protocol._
-import com.nutomic.ensichat.protocol.messages.{ConnectionInfo, Message, MessageBody, MessageHeader}
-import com.nutomic.ensichat.util.Database
-
-import scala.collection.SortedSet
-import scala.collection.immutable.{HashMap, HashSet, TreeSet}
-import scala.concurrent.Future
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.ref.WeakReference
-
-object ChatService {
-
- /**
- * Bluetooth service UUID version 5, created with namespace URL and "ensichat.nutomic.com".
- */
- val appUuid: UUID = UUID.fromString("8ed52b7a-4501-5348-b054-3d94d004656e")
-
- /**
- * Used with [[ChatService.registerConnectionListener]], called when a bluetooth device
- * connects or disconnects
- */
- trait OnNearbyContactsChangedListener {
- def onNearbyContactsChanged(devices: Set[Address]): Unit
- }
-
- trait OnMessageReceivedListener {
- def onMessageReceived(messages: SortedSet[Message]): Unit
- }
-
-}
-
-/**
- * Handles all Bluetooth connectivity.
- */
-class ChatService extends Service {
-
- private val Tag = "ChatService"
-
- private val Binder = new ChatServiceBinder(this)
-
- private var bluetoothAdapter: BluetoothAdapter = _
-
- /**
- * For this (and [[messageListeners]], functions would be useful instead of instances,
- * but on a Nexus S (Android 4.1.2), these functions are garbage collected even when
- * referenced.
- */
- private var connectionListeners = new HashSet[WeakReference[OnNearbyContactsChangedListener]]()
-
- private var messageListeners = Set[WeakReference[OnMessageReceivedListener]]()
-
- private var devices = new HashMap[Device.ID, Device]()
-
- private var connections = new HashMap[Device.ID, TransferThread]()
-
- private var ListenThread: ListenThread = _
-
- private var cancelDiscovery = false
-
- private val MainHandler = new Handler()
-
- private lazy val Database = new Database(this)
-
- private lazy val Crypto = new Crypto(this)
-
- private var discovered = Set[Device]()
-
- private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]()
-
- /**
- * Sets up bluetooth connectivity.
- */
- override def onCreate(): Unit = {
- super.onCreate()
-
- bluetoothAdapter = BluetoothAdapter.getDefaultAdapter
-
- registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
- registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
- registerReceiver(DiscoveryFinishedReceiver,
- new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
-
- Future {
- Crypto.generateLocalKeys()
- startBluetoothConnections()
- Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
- }
- }
-
- override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
-
- override def onBind(intent: Intent) = Binder
-
- /**
- * Stops discovery, listening and unregisters receivers.
- */
- override def onDestroy(): Unit = {
- super.onDestroy()
- ListenThread.cancel()
- cancelDiscovery = true
- unregisterReceiver(DeviceDiscoveredReceiver)
- unregisterReceiver(BluetoothStateReceiver)
- unregisterReceiver(DiscoveryFinishedReceiver)
- }
-
- /**
- * Stops any current discovery, then starts a new one, recursively until service is stopped.
- */
- def discover(): Unit = {
- if (cancelDiscovery)
- return
-
- if (!bluetoothAdapter.isDiscovering) {
- Log.v(Tag, "Starting discovery")
- bluetoothAdapter.startDiscovery()
- }
-
- val scanInterval = PreferenceManager.getDefaultSharedPreferences(this)
- .getString("scan_interval_seconds", "15").toInt * 1000
- MainHandler.postDelayed(new Runnable {
- override def run(): Unit = discover()
- }, scanInterval)
- }
-
- /**
- * Receives newly discovered devices and connects to them.
- */
- private val DeviceDiscoveredReceiver = new BroadcastReceiver() {
- override def onReceive(context: Context, intent: Intent) {
- discovered += new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
- }
- }
-
- /**
- * Iniates the actual connection to discovered devices.
- */
- private val DiscoveryFinishedReceiver = new BroadcastReceiver() {
- override def onReceive(context: Context, intent: Intent): Unit = {
- discovered.filterNot(d => connections.keySet.contains(d.Id))
- .foreach { d =>
- new ConnectThread(d, onConnectionChanged).start()
- devices += (d.Id -> d)
- }
- discovered = Set[Device]()
- }
- }
-
- /**
- * Starts or stops listening and discovery based on bluetooth state.
- */
- private val BluetoothStateReceiver = new BroadcastReceiver {
- override def onReceive(context: Context, intent: Intent): Unit = {
- intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) match {
- case BluetoothAdapter.STATE_ON =>
- if (Crypto.localKeysExist)
- startBluetoothConnections()
- case BluetoothAdapter.STATE_TURNING_OFF =>
- connections.foreach(d => d._2.close())
- case BluetoothAdapter.STATE_OFF =>
- Log.i(Tag, "Bluetooth disabled, stopping listening and discovery")
- if (ListenThread != null) {
- ListenThread.cancel()
- }
- cancelDiscovery = true
- case _ =>
- }
- }
- }
-
- /**
- * Starts to listen for incoming connections, and starts regular active discovery.
- */
- private def startBluetoothConnections(): Unit = {
- cancelDiscovery = false
- discover()
- ListenThread =
- new ListenThread(getString(R.string.app_name), bluetoothAdapter, onConnectionChanged)
- ListenThread.start()
- }
-
- /**
- * Registers a listener that is called whenever a new device is connected.
- */
- def registerConnectionListener(listener: OnNearbyContactsChangedListener): Unit = {
- connectionListeners += new WeakReference[OnNearbyContactsChangedListener](listener)
- nearbyContactsChanged(listener)
- }
-
- /**
- * Called when a Bluetooth device is connected.
- *
- * Adds the device to [[connections]], notifies all [[connectionListeners]], sends DeviceInfoMessage.
- *
- * @param device The updated device info for the remote device.
- * @param socket A socket for data transfer if device.connected is true, otherwise null.
- */
- def onConnectionChanged(device: Device, socket: BluetoothSocket): Unit = {
- devices += (device.Id -> device)
-
- if (device.Connected && !connections.keySet.contains(device.Id)) {
- connections += (device.Id ->
- new TransferThread(device, socket, this, Crypto, onReceiveMessage))
- connections(device.Id).start()
- } else {
- Log.i(Tag, device + " has disconnected")
- AddressDeviceMap.inverse().remove(device.Id)
- }
- }
-
- /**
- * Calls listener with [[devices]] (converting [[Device.ID]]s to [[Address]]es.
- */
- def nearbyContactsChanged(listener: OnNearbyContactsChangedListener) = {
- listener.onNearbyContactsChanged(
- devices.keySet.map(d => AddressDeviceMap.inverse().get(d)).filter(_ != null))
- }
-
- /**
- * Sends a new message to the given target address.
- */
- def sendTo(target: Address, body: MessageBody): Unit = {
- if (!AddressDeviceMap.containsKey(target)) {
- Log.w(Tag, "Receiver " + target + " is not connected, ignoring message")
- return
- }
-
- val header = new MessageHeader(body.Type, MessageHeader.DefaultHopLimit,
- Crypto.getLocalAddress, target, 0, 0)
-
- val msg = new Message(header, body)
- val encrypted = Crypto.encrypt(Crypto.sign(msg))
- connections.apply(AddressDeviceMap.get(target)).send(encrypted)
- Database.addMessage(msg)
- callMessageReceivedListeners(msg)
- }
-
- /**
- * Saves the message to database and sends it to registered listeners.
- *
- * If you want to send a new message, use [[sendTo]].
- *
- * Messages must always be sent between local device and a contact.
- *
- * NOTE: Messages sent from the local node using [[sendTo]] are also passed through this method.
- */
- private def onReceiveMessage(message: Message, device: Device.ID): Unit = {
- assert(message.Header.Origin != Crypto.getLocalAddress)
-
- message.Body match {
- case info: ConnectionInfo =>
- if (message.Header.Origin == Crypto.getLocalAddress)
- return
- onNeighborConnected(info, device)
- case _ =>
- val decrypted = Crypto.decrypt(message)
- if (!Crypto.verify(decrypted)) {
- Log.i(Tag, "Dropping message with invalid signature from " + message.Header.Origin)
- return
- }
-
- callMessageReceivedListeners(decrypted)
- Database.addMessage(decrypted)
- }
- }
-
- /**
- * Calls all [[OnMessageReceivedListener]]s with the new message.
- */
- private def callMessageReceivedListeners(message: Message): Unit = {
- MainHandler.post(new Runnable {
- override def run(): Unit = {
- messageListeners.foreach(l =>
- if (l.get != null)
- l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
- else
- messageListeners -= l)
- }
- })
- }
-
- /**
- * Called when a [[ConnectionInfo]] message from a new neighbor is received.
- */
- private def onNeighborConnected(info: ConnectionInfo, device: Device.ID): Unit = {
- val sender = Crypto.calculateAddress(info.key)
- if (sender == Address.Broadcast || sender == Address.Null) {
- Log.i(Tag, "Received ConnectionInfo message with invalid sender " + sender + ", ignoring")
- return
- }
-
- if (!Crypto.havePublicKey(sender)) {
- Crypto.addPublicKey(sender, info.key)
- Log.i(Tag, "Added public key for new device " + sender.toString)
- }
-
- AddressDeviceMap.put(sender, device)
- Log.i(Tag, "Node " + sender + " connected as " + device)
-
- connectionListeners.foreach(l => l.get match {
- case Some(c) => nearbyContactsChanged(c)
- case None => connectionListeners -= l
- })
-
- }
-
- /**
- * Registers a listener that is called whenever a new message is sent or received.
- */
- def registerMessageListener(listener: OnMessageReceivedListener): Unit = {
- messageListeners += new WeakReference[OnMessageReceivedListener](listener)
- }
-
- /**
- * Returns the unique bluetooth address of the local device.
- */
- private def localDeviceId = new Device.ID(bluetoothAdapter.getAddress)
-
- def database = Database
-
-}
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
index 2727e5e..98a21e5 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
@@ -14,7 +14,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
private val Tag = "ConnectThread"
private val Socket: BluetoothSocket =
- device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid)
+ device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(BluetoothInterface.AppUuid)
override def run(): Unit = {
Log.i(Tag, "Connecting to " + device.toString)
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
index d155f48..2d81850 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
@@ -17,7 +17,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
private val ServerSocket: BluetoothServerSocket =
try {
- adapter.listenUsingInsecureRfcommWithServiceRecord(name, ChatService.appUuid)
+ adapter.listenUsingInsecureRfcommWithServiceRecord(name, BluetoothInterface.AppUuid)
} catch {
case e: IOException =>
Log.e(Tag, "Failed to create listener", e)
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
index 6ad37f3..c507449 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
@@ -16,8 +16,8 @@ import com.nutomic.ensichat.protocol.messages.{ConnectionInfo, Message, MessageH
* @param socket An open socket to the given device.
* @param onReceive Called when a message was received from the other device.
*/
-class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService,
- crypto: Crypto, onReceive: (Message, Device.ID) => Unit)
+class TransferThread(device: Device, socket: BluetoothSocket, Handler: BluetoothInterface,
+ Crypto: Crypto, onReceive: (Message, Device.ID) => Unit)
extends Thread {
private val Tag: String = "TransferThread"
@@ -43,8 +43,8 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
override def run(): Unit = {
Log.i(Tag, "Starting data transfer with " + device.toString)
- send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit,
- Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
+ send(Crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit,
+ Address.Null, Address.Null, 0, 0), new ConnectionInfo(Crypto.getLocalPublicKey))))
while (socket.isConnected) {
try {
@@ -62,8 +62,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
return
}
}
- service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
- Log.i(Tag, "Neighbor " + device + " has disconnected")
+ close()
}
def send(msg: Message): Unit = {
@@ -81,7 +80,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
} catch {
case e: IOException => Log.e(Tag, "Failed to close socket", e);
} finally {
- service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
+ Handler.onConnectionClosed(new Device(device.bluetoothDevice, false), null)
}
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
index 6b8a5d0..00ca4be 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
@@ -9,11 +9,10 @@ import android.widget.TextView.OnEditorActionListener
import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.EnsiChatActivity
-import com.nutomic.ensichat.bluetooth.ChatService
-import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
-import com.nutomic.ensichat.protocol.Address
+import com.nutomic.ensichat.protocol.{ChatService, Address}
+import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.protocol.messages.{Message, Text}
-import com.nutomic.ensichat.util.MessagesAdapter
+import com.nutomic.ensichat.util.{Database, MessagesAdapter}
import scala.collection.SortedSet
@@ -53,7 +52,7 @@ class ChatFragment extends ListFragment with OnClickListener
// Read local device ID from service,
adapter = new MessagesAdapter(getActivity, address)
chatService.registerMessageListener(ChatFragment.this)
- onMessageReceived(chatService.database.getMessages(address, 15))
+ onMessageReceived(chatService.Database.getMessages(address, 15))
if (listView != null) {
listView.setAdapter(adapter)
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala b/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
index 2dca13f..c646960 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
@@ -7,7 +7,7 @@ import android.view._
import android.widget.ListView
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsiChatActivity, MainActivity, SettingsActivity}
-import com.nutomic.ensichat.bluetooth.ChatService
+import com.nutomic.ensichat.protocol.ChatService
import com.nutomic.ensichat.util.DevicesAdapter
/**
@@ -17,7 +17,7 @@ class ContactsFragment extends ListFragment {
private lazy val Adapter = new DevicesAdapter(getActivity)
- private lazy val Database = getActivity.asInstanceOf[EnsiChatActivity].service.database
+ private lazy val Database = getActivity.asInstanceOf[EnsiChatActivity].service.Database
override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState)
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
new file mode 100644
index 0000000..ee08ea8
--- /dev/null
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
@@ -0,0 +1,192 @@
+package com.nutomic.ensichat.protocol
+
+import android.app.Service
+import android.content.Intent
+import android.os.Handler
+import android.util.Log
+import com.nutomic.ensichat.bluetooth.BluetoothInterface
+import com.nutomic.ensichat.protocol.ChatService.{OnMessageReceivedListener, OnConnectionsChangedListener}
+import com.nutomic.ensichat.protocol.messages.{ConnectionInfo, Message, MessageBody, MessageHeader}
+import com.nutomic.ensichat.util.Database
+
+import scala.collection.SortedSet
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+import scala.ref.WeakReference
+
+object ChatService {
+
+ abstract class InterfaceHandler {
+
+ def create(): Unit
+
+ def destroy(): Unit
+
+ def send(msg: Message): Unit
+
+ }
+
+ trait OnMessageReceivedListener {
+ def onMessageReceived(messages: SortedSet[Message]): Unit
+ }
+
+ /**
+ * Used with [[ChatService.registerConnectionListener]], called when a Bluetooth device
+ * connects or disconnects
+ */
+ trait OnConnectionsChangedListener {
+ def onConnectionsChanged(devices: Set[Address]): Unit
+ }
+
+}
+
+/**
+ * High-level handling of all message transfers and callbacks.
+ */
+class ChatService extends Service {
+
+ private val Tag = "ChatService"
+
+ lazy val Database = new Database(this)
+
+ val MainHandler = new Handler()
+
+ private lazy val Binder = new ChatServiceBinder(this)
+
+ private lazy val Crypto = new Crypto(this)
+
+ private lazy val BluetoothInterface = new BluetoothInterface(this, Crypto)
+
+ /**
+ * For this (and [[messageListeners]], functions would be useful instead of instances,
+ * but on a Nexus S (Android 4.1.2), these functions are garbage collected even when
+ * referenced.
+ */
+ private var connectionListeners = Set[WeakReference[OnConnectionsChangedListener]]()
+
+ private var messageListeners = Set[WeakReference[OnMessageReceivedListener]]()
+
+ /**
+ * Generates keys and starts Bluetooth interface.
+ */
+ override def onCreate(): Unit = {
+ super.onCreate()
+
+ Future {
+ Crypto.generateLocalKeys()
+ Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
+
+ BluetoothInterface.create()
+ }
+ }
+
+ override def onDestroy(): Unit = {
+ BluetoothInterface.destroy()
+ }
+
+ override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
+
+ override def onBind(intent: Intent) = Binder
+
+ /**
+ * Registers a listener that is called whenever a new message is sent or received.
+ */
+ def registerMessageListener(listener: OnMessageReceivedListener): Unit = {
+ messageListeners += new WeakReference[OnMessageReceivedListener](listener)
+ }
+
+ /**
+ * Registers a listener that is called whenever a new device is connected.
+ */
+ def registerConnectionListener(listener: OnConnectionsChangedListener): Unit = {
+ connectionListeners += new WeakReference[OnConnectionsChangedListener](listener)
+ listener.onConnectionsChanged(BluetoothInterface.getConnections)
+ }
+
+ /**
+ * Sends a new message to the given target address.
+ */
+ def sendTo(target: Address, body: MessageBody): Unit = {
+ if (!BluetoothInterface.getConnections.contains(target))
+ return
+
+ val header = new MessageHeader(body.Type, MessageHeader.DefaultHopLimit,
+ Crypto.getLocalAddress, target, 0, 0)
+
+ val msg = new Message(header, body)
+ val encrypted = Crypto.encrypt(Crypto.sign(msg))
+ BluetoothInterface.send(encrypted)
+ onNewMessage(msg)
+ }
+
+ /**
+ * Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]].
+ */
+ def onMessageReceived(msg: Message): Unit = {
+ val decrypted = Crypto.decrypt(msg)
+ if (!Crypto.verify(decrypted)) {
+ Log.i(Tag, "Ignoring message with invalid signature from " + msg.Header.Origin)
+ return
+ }
+ onNewMessage(decrypted)
+ }
+
+ /**
+ * Calls all [[OnMessageReceivedListener]]s with the new message.
+ *
+ * This function is called both for locally and remotely sent messages.
+ */
+ private def onNewMessage(msg: Message): Unit = {
+ Database.addMessage(msg)
+ MainHandler.post(new Runnable {
+ override def run(): Unit =
+ messageListeners
+ .filter(_.get.nonEmpty)
+ .foreach(_.apply().onMessageReceived(SortedSet(msg)(Message.Ordering)))
+ })
+ }
+
+ /**
+ * Opens connection to a direct neighbor.
+ *
+ * This adds the other node's public key if we don't have it. If we do, it validates the signature
+ * with the stored key.
+ *
+ * The caller must invoke [[callConnectionListeners()]]
+ *
+ * @param infoMsg The message containing [[ConnectionInfo]] to open the connection.
+ * @return True if the connection is valid
+ */
+ def onConnectionOpened(infoMsg: Message): Option[Address] = {
+ val info = infoMsg.Body.asInstanceOf[ConnectionInfo]
+ val sender = Crypto.calculateAddress(info.key)
+ if (sender == Address.Broadcast || sender == Address.Null) {
+ Log.i(Tag, "Ignoring ConnectionInfo message with invalid sender " + sender)
+ None
+ }
+
+ if (Crypto.havePublicKey(sender) && !Crypto.verify(infoMsg, Crypto.getPublicKey(sender))) {
+ Log.i(Tag, "Ignoring ConnectionInfo message with invalid signature")
+ None
+ }
+
+ if (!Crypto.havePublicKey(sender)) {
+ Crypto.addPublicKey(sender, info.key)
+ Log.i(Tag, "Added public key for new device " + sender.toString)
+ }
+
+ Log.i(Tag, "Node " + sender + " connected")
+ Some(sender)
+ }
+
+ /**
+ * Calls all [[connectionListeners]] with the currently active connections.
+ *
+ * Should be called whenever a neighbor connects or disconnects.
+ */
+ def callConnectionListeners(): Unit =
+ connectionListeners
+ .filter(_ != None)
+ .foreach(_.apply().onConnectionsChanged(BluetoothInterface.getConnections))
+
+}
\ No newline at end of file
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatServiceBinder.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatServiceBinder.scala
similarity index 75%
rename from app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatServiceBinder.scala
rename to app/src/main/scala/com/nutomic/ensichat/protocol/ChatServiceBinder.scala
index 77dccd5..27ca6be 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatServiceBinder.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatServiceBinder.scala
@@ -1,4 +1,4 @@
-package com.nutomic.ensichat.bluetooth
+package com.nutomic.ensichat.protocol
import android.os.Binder