diff --git a/PROTOCOL.md b/PROTOCOL.md
index e1d8bdb..78c7436 100644
--- a/PROTOCOL.md
+++ b/PROTOCOL.md
@@ -113,7 +113,7 @@ message.
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
- \ Key (variable length) \
+ \ Key (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
@@ -124,7 +124,6 @@ Signature is the cryptographic signature over the (unencrypted) message
header and message body.
-
ConnectionInfo (Type = 0)
---------
@@ -142,8 +141,8 @@ that node can only be sent once a new ConnectionInfo message
for it has been received.
-This key is to be used for message
-encryption when communicating with the sending node.
+This key is to be used for message encryption when communicating
+with the sending node.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
@@ -189,6 +188,7 @@ Accepted bit (A) is true if the user accepts the new contact, false
otherwise. Nodes should only add another node as a contact if both
users agreed.
+
### Text (Type = 6)
A simple chat message.
@@ -208,3 +208,17 @@ A simple chat message.
Time is the unix timestamp of message sending.
Text the string to be transferred, encoded as UTF-8.
+
+### UserName (Type = 7)
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Name Length |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ / /
+ \ Name (variable length) \
+ / /
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+Contains the sender's name, which should be used for display to users.
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala
new file mode 100644
index 0000000..2c4a2eb
--- /dev/null
+++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala
@@ -0,0 +1,9 @@
+package com.nutomic.ensichat.protocol
+
+object UserTest {
+
+ val u1 = new User(AddressTest.a1, "one")
+
+ val u2 = new User(AddressTest.a2, "two")
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/UserNameTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/UserNameTest.scala
new file mode 100644
index 0000000..1b9d035
--- /dev/null
+++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/UserNameTest.scala
@@ -0,0 +1,15 @@
+package com.nutomic.ensichat.protocol.messages
+
+import android.test.AndroidTestCase
+import junit.framework.Assert
+
+class UserNameTest extends AndroidTestCase {
+
+ def testWriteRead(): Unit = {
+ val name = new UserName("name")
+ val bytes = name.write
+ val body = UserName.read(bytes)
+ Assert.assertEquals(name, body.asInstanceOf[UserName])
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
index fb74236..c597877 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
+++ b/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
@@ -1,6 +1,5 @@
package com.nutomic.ensichat.util
-import java.io.File
import java.util.concurrent.CountDownLatch
import android.content.Context
@@ -8,8 +7,7 @@ import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.test.AndroidTestCase
import android.test.mock.MockContext
-import com.nutomic.ensichat.protocol.AddressTest
-import com.nutomic.ensichat.protocol.messages.MessageTest
+import com.nutomic.ensichat.protocol.UserTest
import com.nutomic.ensichat.protocol.messages.MessageTest._
import junit.framework.Assert._
@@ -25,9 +23,10 @@ class DatabaseTest extends AndroidTestCase {
private var dbFile: String = _
- private lazy val Database = new Database(new TestContext(getContext))
+ private var Database: Database = _
override def setUp(): Unit = {
+ Database = new Database(new TestContext(getContext))
Database.addMessage(m1)
Database.addMessage(m2)
Database.addMessage(m3)
@@ -35,7 +34,8 @@ class DatabaseTest extends AndroidTestCase {
override def tearDown(): Unit = {
super.tearDown()
- new File(dbFile).delete()
+ Database.close()
+ getContext.deleteDatabase(dbFile)
}
def testMessageCount(): Unit = {
@@ -58,11 +58,10 @@ class DatabaseTest extends AndroidTestCase {
}
def testAddContact(): Unit = {
- Database.addContact(AddressTest.a1)
- assertTrue(Database.isContact(AddressTest.a1))
+ Database.addContact(UserTest.u1)
val contacts = Database.getContacts
assertEquals(1, contacts.size)
- contacts.foreach{assertEquals(AddressTest.a1, _)}
+ assertEquals(Some(UserTest.u1), Database.getContact(UserTest.u1.Address))
}
def testAddContactCallback(): Unit = {
@@ -70,8 +69,16 @@ class DatabaseTest extends AndroidTestCase {
Database.runOnContactsUpdated(() => {
latch.countDown()
})
- Database.addContact(AddressTest.a1)
+ Database.addContact(UserTest.u1)
latch.await()
}
+
+ def testGetContact(): Unit = {
+ Database.addContact(UserTest.u2)
+ assertTrue(Database.getContact(UserTest.u1.Address).isEmpty)
+ val c = Database.getContact(UserTest.u2.Address)
+ assertTrue(c.nonEmpty)
+ assertEquals(Some(UserTest.u2), c)
+ }
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d8e4e6c..82bc89e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -57,6 +57,9 @@
Settings
+
+ Name
+
Scan Interval (seconds)
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index 70712bb..5d81321 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -2,6 +2,10 @@
+
+
// Local user accepted contact, update state and send info to other device.
currentlyAdding +=
- (address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true))
- addContactIfBothConfirmed(address)
- service.sendTo(address, new ResultAddContact(true))
+ (contact -> new AddContactInfo(currentlyAdding(contact).localConfirmed, true))
+ addContactIfBothConfirmed(contact)
+ service.sendTo(contact.Address, new ResultAddContact(true))
case DialogInterface.BUTTON_NEGATIVE =>
// Local user denied adding contact, send info to other device.
- service.sendTo(address, new ResultAddContact(false))
+ service.sendTo(contact.Address, new ResultAddContact(false))
}
}
@@ -117,12 +117,12 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
local.setImageBitmap(
IdenticonGenerator.generate(Crypto.getLocalAddress, (150, 150), this))
val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
- remoteTitle.setText(getString(R.string.remote_fingerprint_title, address))
+ remoteTitle.setText(getString(R.string.remote_fingerprint_title, contact.Name))
val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView]
- remote.setImageBitmap(IdenticonGenerator.generate(address, (150, 150), this))
+ remote.setImageBitmap(IdenticonGenerator.generate(contact.Address, (150, 150), this))
new AlertDialog.Builder(this)
- .setTitle(getString(R.string.add_contact_dialog, address))
+ .setTitle(getString(R.string.add_contact_dialog, contact.Name))
.setView(view)
.setPositiveButton(android.R.string.yes, onClick)
.setNegativeButton(android.R.string.no, onClick)
@@ -140,19 +140,20 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
.foreach{
case m if m.Body.isInstanceOf[RequestAddContact] =>
Log.i(Tag, "Remote device " + m.Header.Origin + " wants to add us as a contact, showing dialog")
- addDeviceDialog(m.Header.Origin)
+ service.getConnections.find(_.Address == m.Header.Origin).foreach(addDeviceDialog)
case m if m.Body.isInstanceOf[ResultAddContact] =>
- val origin = m.Header.Origin
- if (m.Body.asInstanceOf[ResultAddContact].Accepted) {
- Log.i(Tag, "Remote device " + origin + " accepted us as a contact, updating state")
- currentlyAdding += (origin ->
- new AddContactInfo(true, currentlyAdding(origin).remoteConfirmed))
- addContactIfBothConfirmed(origin)
- } else {
- Log.i(Tag, "Remote device " + origin + " denied us as a contact, showing toast")
- Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
- currentlyAdding -= origin
- }
+ currentlyAdding.keys.find(_.Address == m.Header.Origin)foreach(contact =>
+ if (m.Body.asInstanceOf[ResultAddContact].Accepted) {
+ Log.i(Tag, contact.toString + " accepted us as a contact, updating state")
+ currentlyAdding += (contact ->
+ new AddContactInfo(true, currentlyAdding(contact).remoteConfirmed))
+ addContactIfBothConfirmed(contact)
+ } else {
+ Log.i(Tag, contact.toString + " denied us as a contact, showing toast")
+ Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
+ currentlyAdding -= contact
+ }
+ )
case _ =>
}
}
@@ -161,21 +162,21 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
* Add the given device to contacts if [[AddContactInfo.localConfirmed]] and
* [[AddContactInfo.remoteConfirmed]] are true for it in [[currentlyAdding]].
*/
- private def addContactIfBothConfirmed(address: Address): Unit = {
- val info = currentlyAdding(address)
+ private def addContactIfBothConfirmed(contact: User): Unit = {
+ val info = currentlyAdding(contact)
if (info.localConfirmed && info.remoteConfirmed) {
- Log.i(Tag, "Adding new contact " + address.toString)
- Database.addContact(address)
- Toast.makeText(this, getString(R.string.contact_added, address.toString), Toast.LENGTH_SHORT)
+ Log.i(Tag, "Adding new contact " + contact.toString)
+ Database.addContact(contact)
+ Toast.makeText(this, getString(R.string.contact_added, contact.Name), Toast.LENGTH_SHORT)
.show()
- currentlyAdding -= address
+ currentlyAdding -= contact
}
}
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
case android.R.id.home =>
NavUtils.navigateUpFromSameTask(this)
- true;
+ true
case _ =>
super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
index 6ae6cf9..9b05838 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
@@ -96,6 +96,7 @@ class MainActivity extends EnsiChatActivity {
.commit()
currentChat = None
getActionBar.setDisplayHomeAsUpEnabled(false)
+ setTitle(R.string.app_name)
} else
super.onBackPressed()
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
index 9763477..7cf6d92 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
@@ -171,13 +171,11 @@ class BluetoothInterface(Service: ChatService, Crypto: Crypto) extends Interface
*/
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 =>
- }
+ val address = Crypto.calculateAddress(info.key)
+ // Service.onConnectionOpened sends message, so mapping already needs to be in place.
+ AddressDeviceMap.put(address, device)
+ if (!Service.onConnectionOpened(msg))
+ AddressDeviceMap.remove(address)
case _ =>
Service.onMessageReceived(msg)
}
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 00ca4be..4294d7c 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
@@ -9,7 +9,7 @@ import android.widget.TextView.OnEditorActionListener
import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.EnsiChatActivity
-import com.nutomic.ensichat.protocol.{ChatService, Address}
+import com.nutomic.ensichat.protocol.{User, ChatService, Address}
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.protocol.messages.{Message, Text}
import com.nutomic.ensichat.util.{Database, MessagesAdapter}
@@ -49,6 +49,9 @@ class ChatFragment extends ListFragment with OnClickListener
activity.runOnServiceConnected(() => {
chatService = activity.service
+ chatService.Database.getContact(address)
+ .foreach(c => getActivity.setTitle(c.Name))
+
// Read local device ID from service,
adapter = new MessagesAdapter(getActivity, address)
chatService.registerMessageListener(ChatFragment.this)
@@ -114,9 +117,4 @@ class ChatFragment extends ListFragment with OnClickListener
.foreach(adapter.add)
}
- /**
- * Returns the device that this fragment shows chats for.
- */
- def getDevice = address
-
}
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 c646960..1d42b5b 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
@@ -8,14 +8,14 @@ import android.widget.ListView
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsiChatActivity, MainActivity, SettingsActivity}
import com.nutomic.ensichat.protocol.ChatService
-import com.nutomic.ensichat.util.DevicesAdapter
+import com.nutomic.ensichat.util.UsersAdapter
/**
* Lists all nearby, connected devices.
*/
class ContactsFragment extends ListFragment {
- private lazy val Adapter = new DevicesAdapter(getActivity)
+ private lazy val Adapter = new UsersAdapter(getActivity)
private lazy val Database = getActivity.asInstanceOf[EnsiChatActivity].service.Database
@@ -28,8 +28,12 @@ class ContactsFragment extends ListFragment {
getActivity.asInstanceOf[EnsiChatActivity].runOnServiceConnected(() => {
Database.getContacts.foreach(Adapter.add)
Database.runOnContactsUpdated(() => {
- Adapter.clear()
- Database.getContacts.foreach(Adapter.add)
+ getActivity.runOnUiThread(new Runnable {
+ override def run(): Unit = {
+ Adapter.clear()
+ Database.getContacts.foreach(Adapter.add)
+ }
+ })
})
})
}
@@ -64,6 +68,6 @@ class ContactsFragment extends ListFragment {
* Opens a chat with the clicked device.
*/
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit =
- getActivity.asInstanceOf[MainActivity].openChat(Adapter.getItem(position))
+ getActivity.asInstanceOf[MainActivity].openChat(Adapter.getItem(position).Address)
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala b/app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
index 2fd3b2a..8c4f312 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
@@ -1,15 +1,54 @@
package com.nutomic.ensichat.fragments
+import android.content.SharedPreferences
import android.os.Bundle
-import android.preference.PreferenceFragment
+import android.preference.Preference.OnPreferenceChangeListener
+import android.preference.{PreferenceManager, Preference, PreferenceFragment}
+import android.util.Log
import com.nutomic.ensichat.R
+import com.nutomic.ensichat.activities.EnsiChatActivity
+import com.nutomic.ensichat.protocol.Address
+import com.nutomic.ensichat.protocol.messages.UserName
+import com.nutomic.ensichat.fragments.SettingsFragment._
-class SettingsFragment extends PreferenceFragment {
+object SettingsFragment {
+
+ val KeyUserName = "user_name"
+
+ val KeyScanInterval = "scan_interval_seconds"
+
+}
+
+/**
+ * Settings screen.
+ */
+class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListener {
override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.settings)
+ val name = findPreference(KeyUserName)
+ name.setOnPreferenceChangeListener(this)
+ val scanInterval = findPreference(KeyScanInterval)
+ scanInterval.setOnPreferenceChangeListener(this)
+
+ val pm = PreferenceManager.getDefaultSharedPreferences(getActivity)
+ name.setSummary(pm.getString(KeyUserName, ""))
+ scanInterval.setSummary(pm.getString(KeyScanInterval, "15"))
+ }
+
+ /**
+ * Updates summary, sends updated name to contacts.
+ */
+ override def onPreferenceChange(preference: Preference, newValue: AnyRef): Boolean = {
+ if (preference.getKey == KeyUserName) {
+ val service = getActivity.asInstanceOf[EnsiChatActivity].service
+ service.Database.getContacts
+ .foreach(c => service.sendTo(c.Address, new UserName(newValue.toString)))
+ }
+ preference.setSummary(newValue.toString)
+ true
}
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
index ee08ea8..41a59c5 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
@@ -1,15 +1,19 @@
package com.nutomic.ensichat.protocol
import android.app.Service
+import android.bluetooth.BluetoothAdapter
import android.content.Intent
import android.os.Handler
+import android.preference.PreferenceManager
import android.util.Log
import com.nutomic.ensichat.bluetooth.BluetoothInterface
+import com.nutomic.ensichat.fragments.SettingsFragment
import com.nutomic.ensichat.protocol.ChatService.{OnMessageReceivedListener, OnConnectionsChangedListener}
-import com.nutomic.ensichat.protocol.messages.{ConnectionInfo, Message, MessageBody, MessageHeader}
+import com.nutomic.ensichat.protocol.messages._
import com.nutomic.ensichat.util.Database
import scala.collection.SortedSet
+import scala.collection.immutable.HashMap
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.ref.WeakReference
@@ -35,7 +39,7 @@ object ChatService {
* connects or disconnects
*/
trait OnConnectionsChangedListener {
- def onConnectionsChanged(devices: Set[Address]): Unit
+ def onConnectionsChanged(contacts: Set[User]): Unit
}
}
@@ -66,17 +70,29 @@ class ChatService extends Service {
private var messageListeners = Set[WeakReference[OnMessageReceivedListener]]()
+ /**
+ * Holds all known users.
+ *
+ * This is for user names that were received during runtime, and is not persistent.
+ */
+ private var connections = Set[User]()
+
/**
* Generates keys and starts Bluetooth interface.
*/
override def onCreate(): Unit = {
super.onCreate()
+ val pm = PreferenceManager.getDefaultSharedPreferences(this)
+ if (pm.getString(SettingsFragment.KeyUserName, null) == null)
+ pm.edit().putString(SettingsFragment.KeyUserName,
+ BluetoothAdapter.getDefaultAdapter.getName).apply()
+
Future {
Crypto.generateLocalKeys()
- Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
BluetoothInterface.create()
+ Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
}
}
@@ -100,7 +116,7 @@ class ChatService extends Service {
*/
def registerConnectionListener(listener: OnConnectionsChangedListener): Unit = {
connectionListeners += new WeakReference[OnConnectionsChangedListener](listener)
- listener.onConnectionsChanged(BluetoothInterface.getConnections)
+ listener.onConnectionsChanged(getConnections)
}
/**
@@ -132,17 +148,23 @@ class ChatService extends Service {
}
/**
- * Calls all [[OnMessageReceivedListener]]s with the new message.
- *
- * This function is called both for locally and remotely sent messages.
+ * Handles all (locally and remotely sent) new 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)))
+ private def onNewMessage(msg: Message): Unit = msg.Body match {
+ case name: UserName =>
+ val contact = new User(msg.Header.Origin, name.Name)
+ connections += contact
+ if (Database.getContact(msg.Header.Origin).nonEmpty)
+ Database.changeContactName(contact)
+
+ callConnectionListeners()
+ case _ =>
+ Database.addMessage(msg)
+ MainHandler.post(new Runnable {
+ override def run(): Unit =
+ messageListeners
+ .filter(_.get.nonEmpty)
+ .foreach(_.apply().onMessageReceived(SortedSet(msg)(Message.Ordering)))
})
}
@@ -154,20 +176,20 @@ class ChatService extends Service {
*
* The caller must invoke [[callConnectionListeners()]]
*
- * @param infoMsg The message containing [[ConnectionInfo]] to open the connection.
+ * @param msg 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]
+ def onConnectionOpened(msg: Message): Boolean = {
+ val info = msg.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
+ false
}
- if (Crypto.havePublicKey(sender) && !Crypto.verify(infoMsg, Crypto.getPublicKey(sender))) {
+ if (Crypto.havePublicKey(sender) && !Crypto.verify(msg, Crypto.getPublicKey(sender))) {
Log.i(Tag, "Ignoring ConnectionInfo message with invalid signature")
- None
+ false
}
if (!Crypto.havePublicKey(sender)) {
@@ -176,7 +198,10 @@ class ChatService extends Service {
}
Log.i(Tag, "Node " + sender + " connected")
- Some(sender)
+ val name = PreferenceManager.getDefaultSharedPreferences(this).getString("user_name", null)
+ sendTo(sender, new UserName(name))
+ callConnectionListeners()
+ true
}
/**
@@ -184,9 +209,22 @@ class ChatService extends Service {
*
* Should be called whenever a neighbor connects or disconnects.
*/
- def callConnectionListeners(): Unit =
+ def callConnectionListeners(): Unit = {
connectionListeners
- .filter(_ != None)
- .foreach(_.apply().onConnectionsChanged(BluetoothInterface.getConnections))
+ .filter(_.get.nonEmpty)
+ .foreach(_.apply().onConnectionsChanged(getConnections))
+ }
+
+ /**
+ * Returns all direct neighbors.
+ */
+ def getConnections: Set[User] = {
+ BluetoothInterface.getConnections.map{ address =>
+ (Database.getContacts ++ connections).find(_.Address == address) match {
+ case Some(contact) => contact
+ case None => new User(address, address.toString)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
index 81566e9..d1d7b1c 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
@@ -257,11 +257,12 @@ class Crypto(Context: Context) {
// Symmetric decryption of data
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
symmetricCipher.init(Cipher.DECRYPT_MODE, key)
- val decryped = copyThroughCipher(symmetricCipher, msg.Body.asInstanceOf[EncryptedBody].Data)
+ val decrypted = copyThroughCipher(symmetricCipher, msg.Body.asInstanceOf[EncryptedBody].Data)
val body = msg.Header.MessageType match {
- case RequestAddContact.Type => RequestAddContact.read(decryped)
- case ResultAddContact.Type => ResultAddContact.read(decryped)
- case Text.Type => Text.read(decryped)
+ case RequestAddContact.Type => RequestAddContact.read(decrypted)
+ case ResultAddContact.Type => ResultAddContact.read(decrypted)
+ case Text.Type => Text.read(decrypted)
+ case UserName.Type => UserName.read(decrypted)
}
new Message(msg.Header, msg.Crypto, body)
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/User.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/User.scala
new file mode 100644
index 0000000..d7ee3c7
--- /dev/null
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/User.scala
@@ -0,0 +1,4 @@
+package com.nutomic.ensichat.protocol
+
+
+case class User(Address: Address, Name: String)
\ No newline at end of file
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala
index 5b3e74c..b8668cb 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala
@@ -14,6 +14,8 @@ object Message {
}
}
+ val Charset = "UTF-8"
+
def read(stream: InputStream): Message = {
val headerBytes = new Array[Byte](MessageHeader.Length)
stream.read(headerBytes, 0, MessageHeader.Length)
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala
index 70ec214..9cf445f 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala
@@ -9,8 +9,6 @@ object Text {
val Type = 6
- val Charset = "UTF-8"
-
/**
* Constructs [[Text]] instance from byte array.
*/
@@ -20,7 +18,7 @@ object Text {
val length = BufferUtils.getUnsignedInt(b).toInt
val bytes = new Array[Byte](length)
b.get(bytes, 0, length)
- new Text(new String(bytes, Text.Charset), time)
+ new Text(new String(bytes, Message.Charset), time)
}
}
@@ -33,15 +31,15 @@ case class Text(text: String, time: Date = new Date()) extends MessageBody {
override def Type = Text.Type
override def write: Array[Byte] = {
- val bytes = text.getBytes(Text.Charset)
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedInt(b, time.getTime / 1000)
+ val bytes = text.getBytes(Message.Charset)
BufferUtils.putUnsignedInt(b, bytes.length)
b.put(bytes)
b.array()
}
- override def length = 8 + text.getBytes(Text.Charset).length
+ override def length = 8 + text.getBytes(Message.Charset).length
override def equals(a: Any): Boolean = a match {
case o: Text => text == text && time.getTime / 1000 == o.time.getTime / 1000
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala
new file mode 100644
index 0000000..ae6a7e6
--- /dev/null
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala
@@ -0,0 +1,42 @@
+package com.nutomic.ensichat.protocol.messages
+
+import java.nio.ByteBuffer
+
+import com.nutomic.ensichat.protocol.BufferUtils
+
+object UserName {
+
+ val Type = 7
+
+ /**
+ * Constructs [[UserName]] instance from byte array.
+ */
+ def read(array: Array[Byte]): UserName = {
+ val b = ByteBuffer.wrap(array)
+ val length = BufferUtils.getUnsignedInt(b).toInt
+ val bytes = new Array[Byte](length)
+ b.get(bytes, 0, length)
+ new UserName(new String(bytes, Message.Charset))
+ }
+
+}
+
+/**
+ * Holds the display name of the sender.
+ */
+case class UserName(Name: String) extends MessageBody {
+
+ override def Type = UserName.Type
+
+
+ override def write: Array[Byte] = {
+ val b = ByteBuffer.allocate(length)
+ val bytes = Name.getBytes(Message.Charset)
+ BufferUtils.putUnsignedInt(b, bytes.length)
+ b.put(bytes)
+ b.array()
+ }
+
+ override def length = 4 + Name.getBytes(Message.Charset).length
+
+}
\ No newline at end of file
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala
index 8fcec12..345efe6 100644
--- a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala
@@ -4,6 +4,7 @@ import java.util.Date
import android.content.{ContentValues, Context}
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
+import android.util.Log
import com.nutomic.ensichat.protocol._
import com.nutomic.ensichat.protocol.messages._
@@ -25,7 +26,8 @@ object Database {
private val CreateContactsTable = "CREATE TABLE contacts(" +
"_id integer primary key autoincrement," +
- "address text not null)"
+ "address text not null," +
+ "name text not null)"
}
@@ -79,42 +81,60 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
cv.put("date", text.time.getTime.toString)
cv.put("text", text.text)
getWritableDatabase.insert("messages", null, cv)
- case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact =>
+ case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact | _: UserName =>
// Never stored.
}
/**
- * Returns a list of all contacts of this device.
+ * Returns all contacts of this user.
*/
- def getContacts: Set[Address] = {
- val c = getReadableDatabase.query(true, "contacts", Array("address"), "", Array(),
+ def getContacts: Set[User] = {
+ val c = getReadableDatabase.query(true, "contacts", Array("address", "name"), "", Array(),
null, null, null, null)
- var contacts = Set[Address]()
+ var contacts = Set[User]()
while (c.moveToNext()) {
- contacts += new Address(c.getString(c.getColumnIndex("address")))
+ contacts += new User(new Address(c.getString(c.getColumnIndex("address"))),
+ c.getString(c.getColumnIndex("name")))
}
c.close()
contacts
}
/**
- * Returns true if a contact with the given device ID exists.
+ * Returns the contact with the given address if it exists.
*/
- def isContact(address: Address): Boolean = {
- val c = getReadableDatabase.query(true, "contacts", Array("_id"), "address = ?",
+ def getContact(address: Address): Option[User] = {
+ val c = getReadableDatabase.query(true, "contacts", Array("address", "name"), "address = ?",
Array(address.toString), null, null, null, null)
- c.getCount != 0
+ if (c.getCount != 0) {
+ c.moveToNext()
+ val s = Some(new User(new Address(c.getString(c.getColumnIndex("address"))),
+ c.getString(c.getColumnIndex("name"))))
+ c.close()
+ s
+ } else {
+ c.close()
+ None
+ }
}
/**
* Inserts the given device into contacts.
*/
- def addContact(address: Address): Unit = {
+ def addContact(contact: User): Unit = {
val cv = new ContentValues()
- cv.put("address", address.toString)
+ cv.put("address", contact.Address.toString)
+ cv.put("name", contact.Name.toString)
getWritableDatabase.insert("contacts", null, cv)
contactsUpdatedListeners.foreach(_())
}
+
+ def changeContactName(contact: User): Unit = {
+ val cv = new ContentValues()
+ cv.put("name", contact.Name.toString)
+ getWritableDatabase.update("contacts", cv, "address = ?", Array(contact.Address.toString))
+ contactsUpdatedListeners.foreach(_())
+ }
/**
* Pass a callback that is called whenever a new contact is added.
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/DevicesAdapter.scala b/app/src/main/scala/com/nutomic/ensichat/util/UsersAdapter.scala
similarity index 70%
rename from app/src/main/scala/com/nutomic/ensichat/util/DevicesAdapter.scala
rename to app/src/main/scala/com/nutomic/ensichat/util/UsersAdapter.scala
index a16edf3..bfa8114 100644
--- a/app/src/main/scala/com/nutomic/ensichat/util/DevicesAdapter.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/util/UsersAdapter.scala
@@ -4,18 +4,18 @@ import android.content.Context
import android.view.{View, ViewGroup}
import android.widget.{ArrayAdapter, TextView}
import com.nutomic.ensichat.bluetooth.Device
-import com.nutomic.ensichat.protocol.Address
+import com.nutomic.ensichat.protocol.{User, Address}
/**
* Displays [[Device]]s in ListView.
*/
-class DevicesAdapter(context: Context) extends
- ArrayAdapter[Address](context, android.R.layout.simple_list_item_1) {
+class UsersAdapter(context: Context) extends
+ ArrayAdapter[User](context, android.R.layout.simple_list_item_1) {
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
val view = super.getView(position, convertView, parent)
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
- title.setText(getItem(position).toString)
+ title.setText(getItem(position).Name)
view
}