From 5a460c9527e32d79c10f7fe9c9cbf29638a712b5 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 29 Jan 2015 17:33:55 +0100 Subject: [PATCH] Added user name. --- PROTOCOL.md | 22 ++++- .../nutomic/ensichat/protocol/UserTest.scala | 9 ++ .../protocol/messages/UserNameTest.scala | 15 ++++ .../nutomic/ensichat/util/DatabaseTest.scala | 25 ++++-- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/settings.xml | 4 + .../activities/AddContactsActivity.scala | 77 +++++++++-------- .../ensichat/activities/MainActivity.scala | 1 + .../bluetooth/BluetoothInterface.scala | 12 ++- .../ensichat/fragments/ChatFragment.scala | 10 +-- .../ensichat/fragments/ContactsFragment.scala | 14 +-- .../ensichat/fragments/SettingsFragment.scala | 43 +++++++++- .../ensichat/protocol/ChatService.scala | 86 +++++++++++++------ .../nutomic/ensichat/protocol/Crypto.scala | 9 +- .../com/nutomic/ensichat/protocol/User.scala | 4 + .../ensichat/protocol/messages/Message.scala | 2 + .../ensichat/protocol/messages/Text.scala | 8 +- .../ensichat/protocol/messages/UserName.scala | 42 +++++++++ .../com/nutomic/ensichat/util/Database.scala | 46 +++++++--- ...evicesAdapter.scala => UsersAdapter.scala} | 8 +- 20 files changed, 319 insertions(+), 121 deletions(-) create mode 100644 app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala create mode 100644 app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/UserNameTest.scala create mode 100644 app/src/main/scala/com/nutomic/ensichat/protocol/User.scala create mode 100644 app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala rename app/src/main/scala/com/nutomic/ensichat/util/{DevicesAdapter.scala => UsersAdapter.scala} (70%) 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 }