Added user name.
This commit is contained in:
parent
647f946586
commit
5a460c9527
20 changed files with 319 additions and 121 deletions
22
PROTOCOL.md
22
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.
|
header and message body.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ConnectionInfo (Type = 0)
|
ConnectionInfo (Type = 0)
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -142,8 +141,8 @@ that node can only be sent once a new ConnectionInfo message
|
||||||
for it has been received.
|
for it has been received.
|
||||||
|
|
||||||
|
|
||||||
This key is to be used for message
|
This key is to be used for message encryption when communicating
|
||||||
encryption when communicating with the sending node.
|
with the sending node.
|
||||||
|
|
||||||
0 1 2 3
|
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
|
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
|
otherwise. Nodes should only add another node as a contact if both
|
||||||
users agreed.
|
users agreed.
|
||||||
|
|
||||||
|
|
||||||
### Text (Type = 6)
|
### Text (Type = 6)
|
||||||
|
|
||||||
A simple chat message.
|
A simple chat message.
|
||||||
|
@ -208,3 +208,17 @@ A simple chat message.
|
||||||
Time is the unix timestamp of message sending.
|
Time is the unix timestamp of message sending.
|
||||||
|
|
||||||
Text the string to be transferred, encoded as UTF-8.
|
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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
}
|
|
@ -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])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.nutomic.ensichat.util
|
package com.nutomic.ensichat.util
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -8,8 +7,7 @@ import android.database.DatabaseErrorHandler
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.test.AndroidTestCase
|
import android.test.AndroidTestCase
|
||||||
import android.test.mock.MockContext
|
import android.test.mock.MockContext
|
||||||
import com.nutomic.ensichat.protocol.AddressTest
|
import com.nutomic.ensichat.protocol.UserTest
|
||||||
import com.nutomic.ensichat.protocol.messages.MessageTest
|
|
||||||
import com.nutomic.ensichat.protocol.messages.MessageTest._
|
import com.nutomic.ensichat.protocol.messages.MessageTest._
|
||||||
import junit.framework.Assert._
|
import junit.framework.Assert._
|
||||||
|
|
||||||
|
@ -25,9 +23,10 @@ class DatabaseTest extends AndroidTestCase {
|
||||||
|
|
||||||
private var dbFile: String = _
|
private var dbFile: String = _
|
||||||
|
|
||||||
private lazy val Database = new Database(new TestContext(getContext))
|
private var Database: Database = _
|
||||||
|
|
||||||
override def setUp(): Unit = {
|
override def setUp(): Unit = {
|
||||||
|
Database = new Database(new TestContext(getContext))
|
||||||
Database.addMessage(m1)
|
Database.addMessage(m1)
|
||||||
Database.addMessage(m2)
|
Database.addMessage(m2)
|
||||||
Database.addMessage(m3)
|
Database.addMessage(m3)
|
||||||
|
@ -35,7 +34,8 @@ class DatabaseTest extends AndroidTestCase {
|
||||||
|
|
||||||
override def tearDown(): Unit = {
|
override def tearDown(): Unit = {
|
||||||
super.tearDown()
|
super.tearDown()
|
||||||
new File(dbFile).delete()
|
Database.close()
|
||||||
|
getContext.deleteDatabase(dbFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
def testMessageCount(): Unit = {
|
def testMessageCount(): Unit = {
|
||||||
|
@ -58,11 +58,10 @@ class DatabaseTest extends AndroidTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
def testAddContact(): Unit = {
|
def testAddContact(): Unit = {
|
||||||
Database.addContact(AddressTest.a1)
|
Database.addContact(UserTest.u1)
|
||||||
assertTrue(Database.isContact(AddressTest.a1))
|
|
||||||
val contacts = Database.getContacts
|
val contacts = Database.getContacts
|
||||||
assertEquals(1, contacts.size)
|
assertEquals(1, contacts.size)
|
||||||
contacts.foreach{assertEquals(AddressTest.a1, _)}
|
assertEquals(Some(UserTest.u1), Database.getContact(UserTest.u1.Address))
|
||||||
}
|
}
|
||||||
|
|
||||||
def testAddContactCallback(): Unit = {
|
def testAddContactCallback(): Unit = {
|
||||||
|
@ -70,8 +69,16 @@ class DatabaseTest extends AndroidTestCase {
|
||||||
Database.runOnContactsUpdated(() => {
|
Database.runOnContactsUpdated(() => {
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
})
|
})
|
||||||
Database.addContact(AddressTest.a1)
|
Database.addContact(UserTest.u1)
|
||||||
latch.await()
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,9 @@
|
||||||
<!-- Activity title -->
|
<!-- Activity title -->
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
|
||||||
|
<!-- Preference title -->
|
||||||
|
<string name="user_name">Name</string>
|
||||||
|
|
||||||
<!-- Preference title -->
|
<!-- Preference title -->
|
||||||
<string name="scan_interval_seconds">Scan Interval (seconds)</string>
|
<string name="scan_interval_seconds">Scan Interval (seconds)</string>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:title="@string/user_name"
|
||||||
|
android:key="user_name" />
|
||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:title="@string/scan_interval_seconds"
|
android:title="@string/scan_interval_seconds"
|
||||||
android:key="scan_interval_seconds"
|
android:key="scan_interval_seconds"
|
||||||
|
|
|
@ -11,8 +11,8 @@ import android.widget.AdapterView.OnItemClickListener
|
||||||
import android.widget._
|
import android.widget._
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
|
import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
|
||||||
import com.nutomic.ensichat.protocol.{Address, ChatService, Crypto}
|
import com.nutomic.ensichat.protocol.{User, Address, ChatService, Crypto}
|
||||||
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
|
import com.nutomic.ensichat.util.{UsersAdapter, IdenticonGenerator}
|
||||||
|
|
||||||
import scala.collection.SortedSet
|
import scala.collection.SortedSet
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
||||||
|
|
||||||
private val Tag = "AddContactsActivity"
|
private val Tag = "AddContactsActivity"
|
||||||
|
|
||||||
private lazy val Adapter = new DevicesAdapter(this)
|
private lazy val Adapter = new UsersAdapter(this)
|
||||||
|
|
||||||
private lazy val Database = service.Database
|
private lazy val Database = service.Database
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
||||||
/**
|
/**
|
||||||
* Map of devices that should be added.
|
* Map of devices that should be added.
|
||||||
*/
|
*/
|
||||||
private var currentlyAdding = Map[Address, AddContactInfo]()
|
private var currentlyAdding = Map[User, AddContactInfo]()
|
||||||
.withDefaultValue(new AddContactInfo(false, false))
|
.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 localConfirmed If true, the local user has accepted adding the contact.
|
||||||
* @param remoteConfirmed If true, the remote contact has accepted adding this device as contact.
|
* @param remoteConfirmed If true, the remote contact has accepted adding this device as contact.
|
||||||
*/
|
*/
|
||||||
private class AddContactInfo(val localConfirmed: Boolean, val remoteConfirmed: Boolean)
|
private case class AddContactInfo(localConfirmed: Boolean, remoteConfirmed: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes layout, registers connection and message listeners.
|
* 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.
|
* Displays newly connected devices in the list.
|
||||||
*/
|
*/
|
||||||
override def onConnectionsChanged(devices: Set[Address]): Unit = {
|
override def onConnectionsChanged(contacts: Set[User]): Unit = {
|
||||||
runOnUiThread(new Runnable {
|
runOnUiThread(new Runnable {
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
Adapter.clear()
|
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.
|
* 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 = {
|
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
|
||||||
val address = Adapter.getItem(position)
|
val contact = Adapter.getItem(position)
|
||||||
if (Database.isContact(address)) {
|
if (Database.getContact(contact.Address).nonEmpty) {
|
||||||
Toast.makeText(this, R.string.contact_already_added, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.contact_already_added, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service.sendTo(address, new RequestAddContact())
|
service.sendTo(contact.Address, new RequestAddContact())
|
||||||
addDeviceDialog(address)
|
addDeviceDialog(contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a dialog to accept/deny adding a device as a new 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.
|
// Listener for dialog button clicks.
|
||||||
val onClick = new OnClickListener {
|
val onClick = new OnClickListener {
|
||||||
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = i match {
|
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = i match {
|
||||||
case DialogInterface.BUTTON_POSITIVE =>
|
case DialogInterface.BUTTON_POSITIVE =>
|
||||||
// Local user accepted contact, update state and send info to other device.
|
// Local user accepted contact, update state and send info to other device.
|
||||||
currentlyAdding +=
|
currentlyAdding +=
|
||||||
(address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true))
|
(contact -> new AddContactInfo(currentlyAdding(contact).localConfirmed, true))
|
||||||
addContactIfBothConfirmed(address)
|
addContactIfBothConfirmed(contact)
|
||||||
service.sendTo(address, new ResultAddContact(true))
|
service.sendTo(contact.Address, new ResultAddContact(true))
|
||||||
case DialogInterface.BUTTON_NEGATIVE =>
|
case DialogInterface.BUTTON_NEGATIVE =>
|
||||||
// Local user denied adding contact, send info to other device.
|
// 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(
|
local.setImageBitmap(
|
||||||
IdenticonGenerator.generate(Crypto.getLocalAddress, (150, 150), this))
|
IdenticonGenerator.generate(Crypto.getLocalAddress, (150, 150), this))
|
||||||
val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
|
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]
|
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)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle(getString(R.string.add_contact_dialog, address))
|
.setTitle(getString(R.string.add_contact_dialog, contact.Name))
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(android.R.string.yes, onClick)
|
.setPositiveButton(android.R.string.yes, onClick)
|
||||||
.setNegativeButton(android.R.string.no, onClick)
|
.setNegativeButton(android.R.string.no, onClick)
|
||||||
|
@ -140,19 +140,20 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
||||||
.foreach{
|
.foreach{
|
||||||
case m if m.Body.isInstanceOf[RequestAddContact] =>
|
case m if m.Body.isInstanceOf[RequestAddContact] =>
|
||||||
Log.i(Tag, "Remote device " + m.Header.Origin + " wants to add us as a contact, showing dialog")
|
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] =>
|
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) {
|
if (m.Body.asInstanceOf[ResultAddContact].Accepted) {
|
||||||
Log.i(Tag, "Remote device " + origin + " accepted us as a contact, updating state")
|
Log.i(Tag, contact.toString + " accepted us as a contact, updating state")
|
||||||
currentlyAdding += (origin ->
|
currentlyAdding += (contact ->
|
||||||
new AddContactInfo(true, currentlyAdding(origin).remoteConfirmed))
|
new AddContactInfo(true, currentlyAdding(contact).remoteConfirmed))
|
||||||
addContactIfBothConfirmed(origin)
|
addContactIfBothConfirmed(contact)
|
||||||
} else {
|
} 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()
|
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
||||||
currentlyAdding -= origin
|
currentlyAdding -= contact
|
||||||
}
|
}
|
||||||
|
)
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,21 +162,21 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
||||||
* Add the given device to contacts if [[AddContactInfo.localConfirmed]] and
|
* Add the given device to contacts if [[AddContactInfo.localConfirmed]] and
|
||||||
* [[AddContactInfo.remoteConfirmed]] are true for it in [[currentlyAdding]].
|
* [[AddContactInfo.remoteConfirmed]] are true for it in [[currentlyAdding]].
|
||||||
*/
|
*/
|
||||||
private def addContactIfBothConfirmed(address: Address): Unit = {
|
private def addContactIfBothConfirmed(contact: User): Unit = {
|
||||||
val info = currentlyAdding(address)
|
val info = currentlyAdding(contact)
|
||||||
if (info.localConfirmed && info.remoteConfirmed) {
|
if (info.localConfirmed && info.remoteConfirmed) {
|
||||||
Log.i(Tag, "Adding new contact " + address.toString)
|
Log.i(Tag, "Adding new contact " + contact.toString)
|
||||||
Database.addContact(address)
|
Database.addContact(contact)
|
||||||
Toast.makeText(this, getString(R.string.contact_added, address.toString), Toast.LENGTH_SHORT)
|
Toast.makeText(this, getString(R.string.contact_added, contact.Name), Toast.LENGTH_SHORT)
|
||||||
.show()
|
.show()
|
||||||
currentlyAdding -= address
|
currentlyAdding -= contact
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||||
case android.R.id.home =>
|
case android.R.id.home =>
|
||||||
NavUtils.navigateUpFromSameTask(this)
|
NavUtils.navigateUpFromSameTask(this)
|
||||||
true;
|
true
|
||||||
case _ =>
|
case _ =>
|
||||||
super.onOptionsItemSelected(item);
|
super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,7 @@ class MainActivity extends EnsiChatActivity {
|
||||||
.commit()
|
.commit()
|
||||||
currentChat = None
|
currentChat = None
|
||||||
getActionBar.setDisplayHomeAsUpEnabled(false)
|
getActionBar.setDisplayHomeAsUpEnabled(false)
|
||||||
|
setTitle(R.string.app_name)
|
||||||
} else
|
} else
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,13 +171,11 @@ class BluetoothInterface(Service: ChatService, Crypto: Crypto) extends Interface
|
||||||
*/
|
*/
|
||||||
private def onReceiveMessage(msg: Message, device: Device.ID): Unit = msg.Body match {
|
private def onReceiveMessage(msg: Message, device: Device.ID): Unit = msg.Body match {
|
||||||
case info: ConnectionInfo =>
|
case info: ConnectionInfo =>
|
||||||
val sender = Service.onConnectionOpened(msg)
|
val address = Crypto.calculateAddress(info.key)
|
||||||
sender match {
|
// Service.onConnectionOpened sends message, so mapping already needs to be in place.
|
||||||
case Some(s) =>
|
AddressDeviceMap.put(address, device)
|
||||||
AddressDeviceMap.put(Crypto.calculateAddress(info.key), device)
|
if (!Service.onConnectionOpened(msg))
|
||||||
Service.callConnectionListeners()
|
AddressDeviceMap.remove(address)
|
||||||
case None =>
|
|
||||||
}
|
|
||||||
case _ =>
|
case _ =>
|
||||||
Service.onMessageReceived(msg)
|
Service.onMessageReceived(msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import android.widget.TextView.OnEditorActionListener
|
||||||
import android.widget._
|
import android.widget._
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.activities.EnsiChatActivity
|
import com.nutomic.ensichat.activities.EnsiChatActivity
|
||||||
import com.nutomic.ensichat.protocol.{ChatService, Address}
|
import com.nutomic.ensichat.protocol.{User, ChatService, Address}
|
||||||
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
|
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
|
||||||
import com.nutomic.ensichat.protocol.messages.{Message, Text}
|
import com.nutomic.ensichat.protocol.messages.{Message, Text}
|
||||||
import com.nutomic.ensichat.util.{Database, MessagesAdapter}
|
import com.nutomic.ensichat.util.{Database, MessagesAdapter}
|
||||||
|
@ -49,6 +49,9 @@ class ChatFragment extends ListFragment with OnClickListener
|
||||||
activity.runOnServiceConnected(() => {
|
activity.runOnServiceConnected(() => {
|
||||||
chatService = activity.service
|
chatService = activity.service
|
||||||
|
|
||||||
|
chatService.Database.getContact(address)
|
||||||
|
.foreach(c => getActivity.setTitle(c.Name))
|
||||||
|
|
||||||
// Read local device ID from service,
|
// Read local device ID from service,
|
||||||
adapter = new MessagesAdapter(getActivity, address)
|
adapter = new MessagesAdapter(getActivity, address)
|
||||||
chatService.registerMessageListener(ChatFragment.this)
|
chatService.registerMessageListener(ChatFragment.this)
|
||||||
|
@ -114,9 +117,4 @@ class ChatFragment extends ListFragment with OnClickListener
|
||||||
.foreach(adapter.add)
|
.foreach(adapter.add)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the device that this fragment shows chats for.
|
|
||||||
*/
|
|
||||||
def getDevice = address
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import android.widget.ListView
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsiChatActivity, MainActivity, SettingsActivity}
|
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsiChatActivity, MainActivity, SettingsActivity}
|
||||||
import com.nutomic.ensichat.protocol.ChatService
|
import com.nutomic.ensichat.protocol.ChatService
|
||||||
import com.nutomic.ensichat.util.DevicesAdapter
|
import com.nutomic.ensichat.util.UsersAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all nearby, connected devices.
|
* Lists all nearby, connected devices.
|
||||||
*/
|
*/
|
||||||
class ContactsFragment extends ListFragment {
|
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
|
private lazy val Database = getActivity.asInstanceOf[EnsiChatActivity].service.Database
|
||||||
|
|
||||||
|
@ -28,8 +28,12 @@ class ContactsFragment extends ListFragment {
|
||||||
getActivity.asInstanceOf[EnsiChatActivity].runOnServiceConnected(() => {
|
getActivity.asInstanceOf[EnsiChatActivity].runOnServiceConnected(() => {
|
||||||
Database.getContacts.foreach(Adapter.add)
|
Database.getContacts.foreach(Adapter.add)
|
||||||
Database.runOnContactsUpdated(() => {
|
Database.runOnContactsUpdated(() => {
|
||||||
Adapter.clear()
|
getActivity.runOnUiThread(new Runnable {
|
||||||
Database.getContacts.foreach(Adapter.add)
|
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.
|
* Opens a chat with the clicked device.
|
||||||
*/
|
*/
|
||||||
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit =
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,54 @@
|
||||||
package com.nutomic.ensichat.fragments
|
package com.nutomic.ensichat.fragments
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
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.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 = {
|
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.settings)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
package com.nutomic.ensichat.protocol
|
package com.nutomic.ensichat.protocol
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
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.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 com.nutomic.ensichat.util.Database
|
||||||
|
|
||||||
import scala.collection.SortedSet
|
import scala.collection.SortedSet
|
||||||
|
import scala.collection.immutable.HashMap
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.Future
|
||||||
import scala.ref.WeakReference
|
import scala.ref.WeakReference
|
||||||
|
@ -35,7 +39,7 @@ object ChatService {
|
||||||
* connects or disconnects
|
* connects or disconnects
|
||||||
*/
|
*/
|
||||||
trait OnConnectionsChangedListener {
|
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]]()
|
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.
|
* Generates keys and starts Bluetooth interface.
|
||||||
*/
|
*/
|
||||||
override def onCreate(): Unit = {
|
override def onCreate(): Unit = {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
val pm = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
if (pm.getString(SettingsFragment.KeyUserName, null) == null)
|
||||||
|
pm.edit().putString(SettingsFragment.KeyUserName,
|
||||||
|
BluetoothAdapter.getDefaultAdapter.getName).apply()
|
||||||
|
|
||||||
Future {
|
Future {
|
||||||
Crypto.generateLocalKeys()
|
Crypto.generateLocalKeys()
|
||||||
Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
|
|
||||||
|
|
||||||
BluetoothInterface.create()
|
BluetoothInterface.create()
|
||||||
|
Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +116,7 @@ class ChatService extends Service {
|
||||||
*/
|
*/
|
||||||
def registerConnectionListener(listener: OnConnectionsChangedListener): Unit = {
|
def registerConnectionListener(listener: OnConnectionsChangedListener): Unit = {
|
||||||
connectionListeners += new WeakReference[OnConnectionsChangedListener](listener)
|
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.
|
* Handles all (locally and remotely sent) new messages.
|
||||||
*
|
|
||||||
* This function is called both for locally and remotely sent messages.
|
|
||||||
*/
|
*/
|
||||||
private def onNewMessage(msg: Message): Unit = {
|
private def onNewMessage(msg: Message): Unit = msg.Body match {
|
||||||
Database.addMessage(msg)
|
case name: UserName =>
|
||||||
MainHandler.post(new Runnable {
|
val contact = new User(msg.Header.Origin, name.Name)
|
||||||
override def run(): Unit =
|
connections += contact
|
||||||
messageListeners
|
if (Database.getContact(msg.Header.Origin).nonEmpty)
|
||||||
.filter(_.get.nonEmpty)
|
Database.changeContactName(contact)
|
||||||
.foreach(_.apply().onMessageReceived(SortedSet(msg)(Message.Ordering)))
|
|
||||||
|
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()]]
|
* 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
|
* @return True if the connection is valid
|
||||||
*/
|
*/
|
||||||
def onConnectionOpened(infoMsg: Message): Option[Address] = {
|
def onConnectionOpened(msg: Message): Boolean = {
|
||||||
val info = infoMsg.Body.asInstanceOf[ConnectionInfo]
|
val info = msg.Body.asInstanceOf[ConnectionInfo]
|
||||||
val sender = Crypto.calculateAddress(info.key)
|
val sender = Crypto.calculateAddress(info.key)
|
||||||
if (sender == Address.Broadcast || sender == Address.Null) {
|
if (sender == Address.Broadcast || sender == Address.Null) {
|
||||||
Log.i(Tag, "Ignoring ConnectionInfo message with invalid sender " + sender)
|
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")
|
Log.i(Tag, "Ignoring ConnectionInfo message with invalid signature")
|
||||||
None
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Crypto.havePublicKey(sender)) {
|
if (!Crypto.havePublicKey(sender)) {
|
||||||
|
@ -176,7 +198,10 @@ class ChatService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(Tag, "Node " + sender + " connected")
|
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.
|
* Should be called whenever a neighbor connects or disconnects.
|
||||||
*/
|
*/
|
||||||
def callConnectionListeners(): Unit =
|
def callConnectionListeners(): Unit = {
|
||||||
connectionListeners
|
connectionListeners
|
||||||
.filter(_ != None)
|
.filter(_.get.nonEmpty)
|
||||||
.foreach(_.apply().onConnectionsChanged(BluetoothInterface.getConnections))
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -257,11 +257,12 @@ class Crypto(Context: Context) {
|
||||||
// Symmetric decryption of data
|
// Symmetric decryption of data
|
||||||
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
|
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
|
||||||
symmetricCipher.init(Cipher.DECRYPT_MODE, key)
|
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 {
|
val body = msg.Header.MessageType match {
|
||||||
case RequestAddContact.Type => RequestAddContact.read(decryped)
|
case RequestAddContact.Type => RequestAddContact.read(decrypted)
|
||||||
case ResultAddContact.Type => ResultAddContact.read(decryped)
|
case ResultAddContact.Type => ResultAddContact.read(decrypted)
|
||||||
case Text.Type => Text.read(decryped)
|
case Text.Type => Text.read(decrypted)
|
||||||
|
case UserName.Type => UserName.read(decrypted)
|
||||||
}
|
}
|
||||||
new Message(msg.Header, msg.Crypto, body)
|
new Message(msg.Header, msg.Crypto, body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
package com.nutomic.ensichat.protocol
|
||||||
|
|
||||||
|
|
||||||
|
case class User(Address: Address, Name: String)
|
|
@ -14,6 +14,8 @@ object Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Charset = "UTF-8"
|
||||||
|
|
||||||
def read(stream: InputStream): Message = {
|
def read(stream: InputStream): Message = {
|
||||||
val headerBytes = new Array[Byte](MessageHeader.Length)
|
val headerBytes = new Array[Byte](MessageHeader.Length)
|
||||||
stream.read(headerBytes, 0, MessageHeader.Length)
|
stream.read(headerBytes, 0, MessageHeader.Length)
|
||||||
|
|
|
@ -9,8 +9,6 @@ object Text {
|
||||||
|
|
||||||
val Type = 6
|
val Type = 6
|
||||||
|
|
||||||
val Charset = "UTF-8"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs [[Text]] instance from byte array.
|
* Constructs [[Text]] instance from byte array.
|
||||||
*/
|
*/
|
||||||
|
@ -20,7 +18,7 @@ object Text {
|
||||||
val length = BufferUtils.getUnsignedInt(b).toInt
|
val length = BufferUtils.getUnsignedInt(b).toInt
|
||||||
val bytes = new Array[Byte](length)
|
val bytes = new Array[Byte](length)
|
||||||
b.get(bytes, 0, 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 Type = Text.Type
|
||||||
|
|
||||||
override def write: Array[Byte] = {
|
override def write: Array[Byte] = {
|
||||||
val bytes = text.getBytes(Text.Charset)
|
|
||||||
val b = ByteBuffer.allocate(length)
|
val b = ByteBuffer.allocate(length)
|
||||||
BufferUtils.putUnsignedInt(b, time.getTime / 1000)
|
BufferUtils.putUnsignedInt(b, time.getTime / 1000)
|
||||||
|
val bytes = text.getBytes(Message.Charset)
|
||||||
BufferUtils.putUnsignedInt(b, bytes.length)
|
BufferUtils.putUnsignedInt(b, bytes.length)
|
||||||
b.put(bytes)
|
b.put(bytes)
|
||||||
b.array()
|
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 {
|
override def equals(a: Any): Boolean = a match {
|
||||||
case o: Text => text == text && time.getTime / 1000 == o.time.getTime / 1000
|
case o: Text => text == text && time.getTime / 1000 == o.time.getTime / 1000
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import java.util.Date
|
||||||
|
|
||||||
import android.content.{ContentValues, Context}
|
import android.content.{ContentValues, Context}
|
||||||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||||
|
import android.util.Log
|
||||||
import com.nutomic.ensichat.protocol._
|
import com.nutomic.ensichat.protocol._
|
||||||
import com.nutomic.ensichat.protocol.messages._
|
import com.nutomic.ensichat.protocol.messages._
|
||||||
|
|
||||||
|
@ -25,7 +26,8 @@ object Database {
|
||||||
|
|
||||||
private val CreateContactsTable = "CREATE TABLE contacts(" +
|
private val CreateContactsTable = "CREATE TABLE contacts(" +
|
||||||
"_id integer primary key autoincrement," +
|
"_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("date", text.time.getTime.toString)
|
||||||
cv.put("text", text.text)
|
cv.put("text", text.text)
|
||||||
getWritableDatabase.insert("messages", null, cv)
|
getWritableDatabase.insert("messages", null, cv)
|
||||||
case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact =>
|
case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact | _: UserName =>
|
||||||
// Never stored.
|
// Never stored.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of all contacts of this device.
|
* Returns all contacts of this user.
|
||||||
*/
|
*/
|
||||||
def getContacts: Set[Address] = {
|
def getContacts: Set[User] = {
|
||||||
val c = getReadableDatabase.query(true, "contacts", Array("address"), "", Array(),
|
val c = getReadableDatabase.query(true, "contacts", Array("address", "name"), "", Array(),
|
||||||
null, null, null, null)
|
null, null, null, null)
|
||||||
var contacts = Set[Address]()
|
var contacts = Set[User]()
|
||||||
while (c.moveToNext()) {
|
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()
|
c.close()
|
||||||
contacts
|
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 = {
|
def getContact(address: Address): Option[User] = {
|
||||||
val c = getReadableDatabase.query(true, "contacts", Array("_id"), "address = ?",
|
val c = getReadableDatabase.query(true, "contacts", Array("address", "name"), "address = ?",
|
||||||
Array(address.toString), null, null, null, null)
|
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.
|
* Inserts the given device into contacts.
|
||||||
*/
|
*/
|
||||||
def addContact(address: Address): Unit = {
|
def addContact(contact: User): Unit = {
|
||||||
val cv = new ContentValues()
|
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)
|
getWritableDatabase.insert("contacts", null, cv)
|
||||||
contactsUpdatedListeners.foreach(_())
|
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.
|
* Pass a callback that is called whenever a new contact is added.
|
||||||
|
|
|
@ -4,18 +4,18 @@ import android.content.Context
|
||||||
import android.view.{View, ViewGroup}
|
import android.view.{View, ViewGroup}
|
||||||
import android.widget.{ArrayAdapter, TextView}
|
import android.widget.{ArrayAdapter, TextView}
|
||||||
import com.nutomic.ensichat.bluetooth.Device
|
import com.nutomic.ensichat.bluetooth.Device
|
||||||
import com.nutomic.ensichat.protocol.Address
|
import com.nutomic.ensichat.protocol.{User, Address}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays [[Device]]s in ListView.
|
* Displays [[Device]]s in ListView.
|
||||||
*/
|
*/
|
||||||
class DevicesAdapter(context: Context) extends
|
class UsersAdapter(context: Context) extends
|
||||||
ArrayAdapter[Address](context, android.R.layout.simple_list_item_1) {
|
ArrayAdapter[User](context, android.R.layout.simple_list_item_1) {
|
||||||
|
|
||||||
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
||||||
val view = super.getView(position, convertView, parent)
|
val view = super.getView(position, convertView, parent)
|
||||||
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||||
title.setText(getItem(position).toString)
|
title.setText(getItem(position).Name)
|
||||||
view
|
view
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue