Split ChatService class into BluetoothInterface and new ChatService.
This commit is contained in:
parent
591d47ffc3
commit
647f946586
12 changed files with 414 additions and 361 deletions
|
@ -42,7 +42,7 @@
|
|||
android:value=".activities.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<service android:name=".bluetooth.ChatService" />
|
||||
<service android:name=".protocol.ChatService" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.nutomic.ensichat.bluetooth
|
||||
package com.nutomic.ensichat.protocol
|
||||
|
||||
import android.os.Binder
|
||||
|
Reference in a new issue