Added user name.

This commit is contained in:
Felix Ableitner 2015-01-29 17:33:55 +01:00
parent 647f946586
commit 5a460c9527
20 changed files with 319 additions and 121 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -57,6 +57,9 @@
<!-- Activity title -->
<string name="settings">Settings</string>
<!-- Preference title -->
<string name="user_name">Name</string>
<!-- Preference title -->
<string name="scan_interval_seconds">Scan Interval (seconds)</string>

View file

@ -2,6 +2,10 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference
android:title="@string/user_name"
android:key="user_name" />
<EditTextPreference
android:title="@string/scan_interval_seconds"
android:key="scan_interval_seconds"

View file

@ -11,8 +11,8 @@ import android.widget.AdapterView.OnItemClickListener
import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
import com.nutomic.ensichat.protocol.{Address, ChatService, Crypto}
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
import com.nutomic.ensichat.protocol.{User, Address, ChatService, Crypto}
import com.nutomic.ensichat.util.{UsersAdapter, IdenticonGenerator}
import scala.collection.SortedSet
@ -26,7 +26,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
private val Tag = "AddContactsActivity"
private lazy val Adapter = new DevicesAdapter(this)
private lazy val Adapter = new UsersAdapter(this)
private lazy val Database = service.Database
@ -35,7 +35,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
/**
* Map of devices that should be added.
*/
private var currentlyAdding = Map[Address, AddContactInfo]()
private var currentlyAdding = Map[User, AddContactInfo]()
.withDefaultValue(new AddContactInfo(false, false))
/**
@ -44,7 +44,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
* @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 case class AddContactInfo(localConfirmed: Boolean, remoteConfirmed: Boolean)
/**
* Initializes layout, registers connection and message listeners.
@ -68,11 +68,11 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
/**
* Displays newly connected devices in the list.
*/
override def onConnectionsChanged(devices: Set[Address]): Unit = {
override def onConnectionsChanged(contacts: Set[User]): Unit = {
runOnUiThread(new Runnable {
override def run(): Unit = {
Adapter.clear()
devices.foreach(Adapter.add)
contacts.foreach(Adapter.add)
}
})
}
@ -81,32 +81,32 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
* Initiates adding the device as contact if it hasn't been added yet.
*/
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
val address = Adapter.getItem(position)
if (Database.isContact(address)) {
val contact = Adapter.getItem(position)
if (Database.getContact(contact.Address).nonEmpty) {
Toast.makeText(this, R.string.contact_already_added, Toast.LENGTH_SHORT).show()
return
}
service.sendTo(address, new RequestAddContact())
addDeviceDialog(address)
service.sendTo(contact.Address, new RequestAddContact())
addDeviceDialog(contact)
}
/**
* Shows a dialog to accept/deny adding a device as a new contact.
*/
private def addDeviceDialog(address: Address): Unit = {
private def addDeviceDialog(contact: User): Unit = {
// Listener for dialog button clicks.
val onClick = new OnClickListener {
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = i match {
case DialogInterface.BUTTON_POSITIVE =>
// 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
currentlyAdding.keys.find(_.Address == m.Header.Origin)foreach(contact =>
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)
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, "Remote device " + origin + " denied us as a contact, showing toast")
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 -= origin
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);
}

View file

@ -96,6 +96,7 @@ class MainActivity extends EnsiChatActivity {
.commit()
currentChat = None
getActionBar.setDisplayHomeAsUpEnabled(false)
setTitle(R.string.app_name)
} else
super.onBackPressed()
}

View file

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

View file

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

View file

@ -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(() => {
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)
}

View file

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

View file

@ -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,11 +148,17 @@ 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 = {
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 =
@ -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)
}
}
}
}

View file

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

View file

@ -0,0 +1,4 @@
package com.nutomic.ensichat.protocol
case class User(Address: Address, Name: String)

View file

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

View file

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

View file

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

View file

@ -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,43 +81,61 @@ 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.
*/

View file

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