Split ChatService class into BluetoothInterface and new ChatService.

This commit is contained in:
Felix Ableitner 2015-01-28 02:09:47 +01:00
parent 591d47ffc3
commit 647f946586
12 changed files with 414 additions and 361 deletions

View file

@ -42,7 +42,7 @@
android:value=".activities.MainActivity" /> android:value=".activities.MainActivity" />
</activity> </activity>
<service android:name=".bluetooth.ChatService" /> <service android:name=".protocol.ChatService" />
</application> </application>

View file

@ -10,10 +10,8 @@ import android.view._
import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemClickListener
import android.widget._ import android.widget._
import com.nutomic.ensichat.R 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.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 com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
import scala.collection.SortedSet import scala.collection.SortedSet
@ -23,14 +21,14 @@ import scala.collection.SortedSet
* *
* Adding a contact requires confirmation on both sides. * Adding a contact requires confirmation on both sides.
*/ */
class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyContactsChangedListener class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnectionsChangedListener
with OnItemClickListener with OnMessageReceivedListener { with OnItemClickListener with ChatService.OnMessageReceivedListener {
private val Tag = "AddContactsActivity" private val Tag = "AddContactsActivity"
private lazy val Adapter = new DevicesAdapter(this) 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) 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 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. * @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. * 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. * Displays newly connected devices in the list.
*/ */
override def onNearbyContactsChanged(devices: Set[Address]): Unit = { override def onConnectionsChanged(devices: Set[Address]): Unit = {
runOnUiThread(new Runnable { runOnUiThread(new Runnable {
override def run(): Unit = { override def run(): Unit = {
Adapter.clear() Adapter.clear()

View file

@ -3,7 +3,8 @@ package com.nutomic.ensichat.activities
import android.app.Activity import android.app.Activity
import android.content.{ComponentName, Context, Intent, ServiceConnection} import android.content.{ComponentName, Context, Intent, ServiceConnection}
import android.os.{Bundle, IBinder} 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. * Connects to [[ChatService]] and provides access to it.

View file

@ -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
}

View file

@ -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
}

View file

@ -14,7 +14,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
private val Tag = "ConnectThread" private val Tag = "ConnectThread"
private val Socket: BluetoothSocket = private val Socket: BluetoothSocket =
device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid) device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(BluetoothInterface.AppUuid)
override def run(): Unit = { override def run(): Unit = {
Log.i(Tag, "Connecting to " + device.toString) Log.i(Tag, "Connecting to " + device.toString)

View file

@ -17,7 +17,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
private val ServerSocket: BluetoothServerSocket = private val ServerSocket: BluetoothServerSocket =
try { try {
adapter.listenUsingInsecureRfcommWithServiceRecord(name, ChatService.appUuid) adapter.listenUsingInsecureRfcommWithServiceRecord(name, BluetoothInterface.AppUuid)
} catch { } catch {
case e: IOException => case e: IOException =>
Log.e(Tag, "Failed to create listener", e) Log.e(Tag, "Failed to create listener", e)

View file

@ -16,8 +16,8 @@ import com.nutomic.ensichat.protocol.messages.{ConnectionInfo, Message, MessageH
* @param socket An open socket to the given device. * @param socket An open socket to the given device.
* @param onReceive Called when a message was received from the other device. * @param onReceive Called when a message was received from the other device.
*/ */
class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService, class TransferThread(device: Device, socket: BluetoothSocket, Handler: BluetoothInterface,
crypto: Crypto, onReceive: (Message, Device.ID) => Unit) Crypto: Crypto, onReceive: (Message, Device.ID) => Unit)
extends Thread { extends Thread {
private val Tag: String = "TransferThread" private val Tag: String = "TransferThread"
@ -43,8 +43,8 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
override def run(): Unit = { override def run(): Unit = {
Log.i(Tag, "Starting data transfer with " + device.toString) Log.i(Tag, "Starting data transfer with " + device.toString)
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit, send(Crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit,
Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey)))) Address.Null, Address.Null, 0, 0), new ConnectionInfo(Crypto.getLocalPublicKey))))
while (socket.isConnected) { while (socket.isConnected) {
try { try {
@ -62,8 +62,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
return return
} }
} }
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null) close()
Log.i(Tag, "Neighbor " + device + " has disconnected")
} }
def send(msg: Message): Unit = { def send(msg: Message): Unit = {
@ -81,7 +80,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
} catch { } catch {
case e: IOException => Log.e(Tag, "Failed to close socket", e); case e: IOException => Log.e(Tag, "Failed to close socket", e);
} finally { } finally {
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null) Handler.onConnectionClosed(new Device(device.bluetoothDevice, false), null)
} }
} }

View file

@ -9,11 +9,10 @@ import android.widget.TextView.OnEditorActionListener
import android.widget._ import android.widget._
import com.nutomic.ensichat.R import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.EnsiChatActivity import com.nutomic.ensichat.activities.EnsiChatActivity
import com.nutomic.ensichat.bluetooth.ChatService import com.nutomic.ensichat.protocol.{ChatService, Address}
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.protocol.Address
import com.nutomic.ensichat.protocol.messages.{Message, Text} 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 import scala.collection.SortedSet
@ -53,7 +52,7 @@ class ChatFragment extends ListFragment with OnClickListener
// Read local device ID from service, // Read local device ID from service,
adapter = new MessagesAdapter(getActivity, address) adapter = new MessagesAdapter(getActivity, address)
chatService.registerMessageListener(ChatFragment.this) chatService.registerMessageListener(ChatFragment.this)
onMessageReceived(chatService.database.getMessages(address, 15)) onMessageReceived(chatService.Database.getMessages(address, 15))
if (listView != null) { if (listView != null) {
listView.setAdapter(adapter) listView.setAdapter(adapter)

View file

@ -7,7 +7,7 @@ import android.view._
import android.widget.ListView import android.widget.ListView
import com.nutomic.ensichat.R import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsiChatActivity, MainActivity, SettingsActivity} 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 import com.nutomic.ensichat.util.DevicesAdapter
/** /**
@ -17,7 +17,7 @@ class ContactsFragment extends ListFragment {
private lazy val Adapter = new DevicesAdapter(getActivity) 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 = { override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -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))
}

View file

@ -1,4 +1,4 @@
package com.nutomic.ensichat.bluetooth package com.nutomic.ensichat.protocol
import android.os.Binder import android.os.Binder