Switch to Slick with H2 as database.
This allows us to use the same database implementation for Android and servers.
This commit is contained in:
parent
5d3720b7e9
commit
656f52d3f3
19 changed files with 294 additions and 383 deletions
|
@ -1,139 +0,0 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
import android.content._
|
||||
import android.database.DatabaseErrorHandler
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.core.body.{CryptoData, Text}
|
||||
import com.nutomic.ensichat.core.header.ContentHeader
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
import com.nutomic.ensichat.util.DatabaseTest._
|
||||
import junit.framework.Assert._
|
||||
|
||||
import scala.collection.SortedSet
|
||||
import scala.collection.immutable.TreeSet
|
||||
|
||||
object DatabaseTest {
|
||||
|
||||
/**
|
||||
* Provides a temporary database file that can be deleted easily.
|
||||
*/
|
||||
private class DatabaseContext(context: Context) extends ContextWrapper(context) {
|
||||
private val dbFile = "database-test.db"
|
||||
override def openOrCreateDatabase(file: String, mode: Int, factory:
|
||||
SQLiteDatabase.CursorFactory, errorHandler: DatabaseErrorHandler) =
|
||||
context.openOrCreateDatabase(dbFile, mode, factory, errorHandler)
|
||||
def deleteDbFile() = context.deleteDatabase(dbFile)
|
||||
}
|
||||
|
||||
private val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
|
||||
private val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
|
||||
private val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
||||
private val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
||||
|
||||
private val h1 = new ContentHeader(a1, a2, 1234, Text.Type, Some(123),
|
||||
Some(new GregorianCalendar(1970, 1, 1).getTime), 5)
|
||||
private val h2 = new ContentHeader(a1, a3, 30000, Text.Type, Some(8765),
|
||||
Some(new GregorianCalendar(2014, 6, 10).getTime), 20)
|
||||
private val h3 = new ContentHeader(a4, a2, 250, Text.Type, Some(77),
|
||||
Some(new GregorianCalendar(2020, 11, 11).getTime), 123)
|
||||
|
||||
private val m1 = new Message(h1, new Text("first"))
|
||||
private val m2 = new Message(h2, new Text("second"))
|
||||
private val m3 = new Message(h3, new Text("third"))
|
||||
|
||||
private val u1 = new User(a1, "one", "s1")
|
||||
private val u2 = new User(a2, "two", "s2")
|
||||
|
||||
}
|
||||
|
||||
class DatabaseTest extends AndroidTestCase {
|
||||
|
||||
private lazy val context = new DatabaseTest.DatabaseContext(getContext)
|
||||
|
||||
private lazy val database = new Database(context)
|
||||
|
||||
override def setUp(): Unit = {
|
||||
super.setUp()
|
||||
database.onMessageReceived(m1)
|
||||
database.onMessageReceived(m2)
|
||||
database.onMessageReceived(m3)
|
||||
}
|
||||
|
||||
override def tearDown(): Unit = {
|
||||
super.tearDown()
|
||||
database.close()
|
||||
context.deleteDbFile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls [[Database.getMessagesCursor]] with parameters and converts the result to sorted set.
|
||||
*/
|
||||
private def getMessages(address: Address): SortedSet[Message] = {
|
||||
val c = database.getMessagesCursor(address)
|
||||
var messages = new TreeSet[Message]()(Message.Ordering)
|
||||
while (c.moveToNext()) {
|
||||
messages += Database.messageFromCursor(c)
|
||||
}
|
||||
c.close()
|
||||
messages
|
||||
}
|
||||
|
||||
def testMessageOrder(): Unit = {
|
||||
val msg = getMessages(m1.header.target).firstKey
|
||||
assertEquals(m1.body, msg.body)
|
||||
}
|
||||
|
||||
def testMessageSelect(): Unit = {
|
||||
val msg = getMessages(m1.header.target)
|
||||
assertTrue(msg.contains(m1))
|
||||
assertFalse(msg.contains(m2))
|
||||
assertTrue(msg.contains(m3))
|
||||
}
|
||||
|
||||
def testMessageFields(): Unit = {
|
||||
val msg = getMessages(m2.header.target).firstKey
|
||||
val header = msg.header.asInstanceOf[ContentHeader]
|
||||
|
||||
assertEquals(h2.origin, header.origin)
|
||||
assertEquals(h2.target, header.target)
|
||||
assertEquals(-1, msg.header.seqNum)
|
||||
assertEquals(h2.contentType, header.contentType)
|
||||
assertEquals(h2.messageId, header.messageId)
|
||||
assertEquals(h2.time, header.time)
|
||||
assertEquals(new CryptoData(None, None), msg.crypto)
|
||||
assertEquals(m2.body, msg.body)
|
||||
}
|
||||
|
||||
def testAddContact(): Unit = {
|
||||
database.addContact(u1)
|
||||
val contacts = database.getContacts
|
||||
assertEquals(1, contacts.size)
|
||||
assertEquals(Option(u1), database.getContact(u1.address))
|
||||
}
|
||||
|
||||
def testAddContactCallback(): Unit = {
|
||||
val latch = new CountDownLatch(1)
|
||||
val lbm = LocalBroadcastManager.getInstance(context)
|
||||
val receiver = new BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = latch.countDown()
|
||||
}
|
||||
lbm.registerReceiver(receiver, new IntentFilter(Database.ActionContactsUpdated))
|
||||
database.addContact(u1)
|
||||
latch.await()
|
||||
lbm.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
def testGetContact(): Unit = {
|
||||
database.addContact(u2)
|
||||
assertTrue(database.getContact(u1.address).isEmpty)
|
||||
val c = database.getContact(u2.address)
|
||||
assertTrue(c.nonEmpty)
|
||||
assertEquals(Option(u2), c)
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,6 @@ import com.google.zxing.integration.android.IntentIntegrator
|
|||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.Address
|
||||
import com.nutomic.ensichat.service.CallbackHandler
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.views.UsersAdapter
|
||||
|
||||
/**
|
||||
|
@ -22,8 +21,6 @@ import com.nutomic.ensichat.views.UsersAdapter
|
|||
*/
|
||||
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
||||
|
||||
private lazy val database = new Database(this)
|
||||
|
||||
private lazy val adapter = new UsersAdapter(this)
|
||||
|
||||
/**
|
||||
|
@ -41,7 +38,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
|||
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(CallbackHandler.ActionConnectionsChanged)
|
||||
filter.addAction(Database.ActionContactsUpdated)
|
||||
filter.addAction(CallbackHandler.ActionContactsUpdated)
|
||||
LocalBroadcastManager.getInstance(this)
|
||||
.registerReceiver(onContactsUpdatedReceiver, filter)
|
||||
}
|
||||
|
@ -120,7 +117,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
|||
|
||||
val user = service.get.getUser(parsedAddress)
|
||||
|
||||
if (database.getContacts.map(_.address).contains(user.address)) {
|
||||
if (database.get.getContacts.map(_.address).contains(user.address)) {
|
||||
val text = getString(R.string.contact_already_added, user.name)
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
|
@ -130,7 +127,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
|||
.setMessage(getString(R.string.dialog_add_contact, user.name))
|
||||
.setPositiveButton(android.R.string.yes, new OnClickListener {
|
||||
override def onClick(dialog: DialogInterface, which: Int): Unit = {
|
||||
database.addContact(user)
|
||||
database.get.addContact(user)
|
||||
Toast.makeText(ConnectionsActivity.this, R.string.toast_contact_added, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
|
|
@ -62,4 +62,6 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
|
|||
*/
|
||||
def service = chatService.map(_.getConnectionHandler)
|
||||
|
||||
def database = chatService.map(_.database)
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import com.nutomic.ensichat.activities.EnsichatActivity
|
|||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.{Address, ConnectionHandler, Message}
|
||||
import com.nutomic.ensichat.service.CallbackHandler
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter}
|
||||
|
||||
/**
|
||||
|
@ -31,8 +30,6 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
this.address = address
|
||||
}
|
||||
|
||||
private lazy val database = new Database(getActivity)
|
||||
|
||||
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
|
||||
private var address: Address = _
|
||||
|
@ -53,10 +50,10 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
activity.runOnServiceConnected(() => {
|
||||
chatService = activity.service.get
|
||||
|
||||
database.getContact(address).foreach(c => getActivity.setTitle(c.name))
|
||||
activity.database.get.getContact(address).foreach(c => getActivity.setTitle(c.name))
|
||||
|
||||
adapter = new DatesAdapter(getActivity,
|
||||
new MessagesAdapter(getActivity, database.getMessagesCursor(address), address))
|
||||
new MessagesAdapter(getActivity, activity.database.get.getMessages(address), address))
|
||||
|
||||
if (listView != null) {
|
||||
listView.setAdapter(adapter)
|
||||
|
@ -131,7 +128,8 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
|
||||
msg.body match {
|
||||
case _: Text =>
|
||||
adapter.changeCursor(database.getMessagesCursor(address))
|
||||
val messages = activity.database.get.getMessages(address)
|
||||
adapter.replaceItems(messages)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import com.nutomic.ensichat.R
|
|||
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||
import com.nutomic.ensichat.service.{CallbackHandler, ChatService}
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.views.UsersAdapter
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
@ -30,7 +29,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
|
|||
|
||||
private lazy val adapter = new UsersAdapter(getActivity)
|
||||
|
||||
private lazy val database = new Database(getActivity)
|
||||
private lazy val database = activity.database.get
|
||||
|
||||
private lazy val lbm = LocalBroadcastManager.getInstance(getActivity)
|
||||
|
||||
|
@ -44,7 +43,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
|
|||
|
||||
setListAdapter(adapter)
|
||||
setHasOptionsMenu(true)
|
||||
lbm.registerReceiver(onContactsUpdatedListener, new IntentFilter(Database.ActionContactsUpdated))
|
||||
lbm.registerReceiver(onContactsUpdatedListener, new IntentFilter(CallbackHandler.ActionContactsUpdated))
|
||||
lbm.registerReceiver(onConnectionsChangedListener, new IntentFilter(CallbackHandler.ActionConnectionsChanged))
|
||||
}
|
||||
|
||||
|
@ -52,7 +51,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
|
|||
super.onResume()
|
||||
activity.runOnServiceConnected(() => {
|
||||
adapter.clear()
|
||||
database.getContacts.foreach(adapter.add)
|
||||
adapter.addAll(database.getContacts)
|
||||
updateConnections()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import com.nutomic.ensichat.core.body.UserInfo
|
|||
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
||||
import com.nutomic.ensichat.fragments.SettingsFragment._
|
||||
import com.nutomic.ensichat.service.ChatService
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.{BuildConfig, R}
|
||||
|
||||
object SettingsFragment {
|
||||
|
@ -21,7 +20,7 @@ object SettingsFragment {
|
|||
*/
|
||||
class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
|
||||
|
||||
private lazy val database = new Database(getActivity)
|
||||
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
|
||||
private lazy val maxConnections = findPreference(KeyMaxConnections)
|
||||
private lazy val version = findPreference(Version)
|
||||
|
@ -52,9 +51,8 @@ class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeL
|
|||
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
key match {
|
||||
case KeyUserName | KeyUserStatus =>
|
||||
val service = getActivity.asInstanceOf[EnsichatActivity].service
|
||||
val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, ""))
|
||||
database.getContacts.foreach(c => service.get.sendTo(c.address, ui))
|
||||
activity.database.get.getContacts.foreach(c => activity.service.get.sendTo(c.address, ui))
|
||||
case KeyServers =>
|
||||
val intent = new Intent(getActivity, classOf[ChatService])
|
||||
intent.setAction(ChatService.ActionNetworkChanged)
|
||||
|
|
|
@ -10,6 +10,7 @@ object CallbackHandler {
|
|||
|
||||
val ActionMessageReceived = "message_received"
|
||||
val ActionConnectionsChanged = "connections_changed"
|
||||
val ActionContactsUpdated = "contacts_updated"
|
||||
|
||||
val ExtraMessage = "extra_message"
|
||||
|
||||
|
@ -38,4 +39,10 @@ class CallbackHandler(chatService: ChatService, notificationHandler: Notificatio
|
|||
.updatePersistentNotification(chatService.getConnectionHandler.connections().size)
|
||||
}
|
||||
|
||||
def onContactsUpdated(): Unit = {
|
||||
val i = new Intent(ActionContactsUpdated)
|
||||
LocalBroadcastManager.getInstance(chatService)
|
||||
.sendBroadcast(i)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import android.content.{Context, Intent, IntentFilter}
|
|||
import android.net.ConnectivityManager
|
||||
import android.os.Handler
|
||||
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
||||
import com.nutomic.ensichat.core.util.Database
|
||||
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
|
||||
import com.nutomic.ensichat.util.{Database, NetworkChangedReceiver, SettingsWrapper}
|
||||
import com.nutomic.ensichat.util.{NetworkChangedReceiver, SettingsWrapper}
|
||||
|
||||
object ChatService {
|
||||
|
||||
|
@ -30,8 +31,10 @@ class ChatService extends Service {
|
|||
|
||||
private val callbackHandler = new CallbackHandler(this, notificationHandler)
|
||||
|
||||
lazy val database = new Database(getDatabasePath("database"), callbackHandler)
|
||||
|
||||
private lazy val connectionHandler =
|
||||
new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
|
||||
new ConnectionHandler(new SettingsWrapper(this), database, callbackHandler,
|
||||
ChatService.newCrypto(this), 1)
|
||||
|
||||
private val networkReceiver = new NetworkChangedReceiver()
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import android.content.{ContentValues, Context, Intent}
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.header.ContentHeader
|
||||
import com.nutomic.ensichat.core.interfaces.DatabaseInterface
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
|
||||
object Database {
|
||||
|
||||
val ActionContactsUpdated = "contacts_updated"
|
||||
|
||||
private val DatabaseName = "message_store.db"
|
||||
|
||||
private val DatabaseVersion = 2
|
||||
|
||||
// NOTE: We could make origin/target foreign keys to contacts, but:
|
||||
// - they don't change anyway
|
||||
// - we'd have to insert the local user into contacts
|
||||
private val CreateMessagesTable = "CREATE TABLE messages(" +
|
||||
"_id INTEGER PRIMARY KEY," +
|
||||
"origin TEXT NOT NULL," +
|
||||
"target TEXT NOT NULL," +
|
||||
"message_id INT NOT NULL," +
|
||||
"text TEXT NOT NULL," +
|
||||
"date INT NOT NULL);" // Unix timestamp
|
||||
|
||||
private val CreateContactsTable = "CREATE TABLE contacts(" +
|
||||
"_id INTEGER PRIMARY KEY," +
|
||||
"address TEXT NOT NULL," +
|
||||
"name TEXT NOT NULL," +
|
||||
"status TEXT NOT NULL)"
|
||||
|
||||
def messageFromCursor(c: Cursor): Message = {
|
||||
val header = new ContentHeader(new Address(
|
||||
c.getString(c.getColumnIndex("origin"))),
|
||||
new Address(c.getString(c.getColumnIndex("target"))),
|
||||
-1,
|
||||
Text.Type,
|
||||
Some(c.getLong(c.getColumnIndex("message_id"))),
|
||||
Some(new Date(c.getLong(c.getColumnIndex("date")))))
|
||||
val body = new Text(new String(c.getString(c.getColumnIndex ("text"))))
|
||||
new Message(header, body)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all messages and contacts in SQL database.
|
||||
*/
|
||||
class Database(context: Context) extends DatabaseInterface {
|
||||
|
||||
private class Helper
|
||||
extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) {
|
||||
|
||||
override def onCreate(db: SQLiteDatabase): Unit = {
|
||||
db.execSQL(Database.CreateContactsTable)
|
||||
db.execSQL(Database.CreateMessagesTable)
|
||||
}
|
||||
|
||||
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
|
||||
if (oldVersion < 2) {
|
||||
db.execSQL("ALTER TABLE contacts ADD COLUMN status TEXT")
|
||||
val cv = new ContentValues()
|
||||
cv.put("status", "")
|
||||
db.update("contacts", cv, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val helper = new Helper()
|
||||
|
||||
def close() = helper.close()
|
||||
|
||||
def getMessagesCursor(address: Address): Cursor = {
|
||||
helper.getReadableDatabase.query(true,
|
||||
"messages", Array("_id", "origin", "target", "message_id", "text", "date"),
|
||||
"origin = ? OR target = ?", Array(address.toString, address.toString),
|
||||
null, null, "date ASC", null)
|
||||
}
|
||||
|
||||
def onMessageReceived(msg: Message): Unit = msg.body match {
|
||||
case text: Text =>
|
||||
val cv = new ContentValues()
|
||||
cv.put("origin", msg.header.origin.toString)
|
||||
cv.put("target", msg.header.target.toString)
|
||||
// Need to use [[Long#toString]] because of https://issues.scala-lang.org/browse/SI-2991
|
||||
cv.put("message_id", msg.header.messageId.get.toString)
|
||||
cv.put("date", msg.header.time.get.getTime.toString)
|
||||
cv.put("text", text.text)
|
||||
helper.getWritableDatabase.insert("messages", null, cv)
|
||||
}
|
||||
|
||||
def getContacts: Set[User] = {
|
||||
val c = helper.getReadableDatabase.query(true, "contacts", Array("address", "name", "status"), "", Array(),
|
||||
null, null, null, null)
|
||||
var contacts = Set[User]()
|
||||
while (c.moveToNext()) {
|
||||
contacts += new User(new Address(c.getString(c.getColumnIndex("address"))),
|
||||
c.getString(c.getColumnIndex("name")),
|
||||
c.getString(c.getColumnIndex("status")))
|
||||
}
|
||||
c.close()
|
||||
contacts
|
||||
}
|
||||
|
||||
def getContact(address: Address): Option[User] = {
|
||||
val c = helper.getReadableDatabase.query(true, "contacts", Array("address", "name", "status"),
|
||||
"address = ?", Array(address.toString), null, null, null, null)
|
||||
if (c.getCount != 0) {
|
||||
c.moveToNext()
|
||||
val s = Option(new User(new Address(c.getString(c.getColumnIndex("address"))),
|
||||
c.getString(c.getColumnIndex("name")),
|
||||
c.getString(c.getColumnIndex("status"))))
|
||||
c.close()
|
||||
s
|
||||
} else {
|
||||
c.close()
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
def addContact(contact: User): Unit = {
|
||||
val cv = new ContentValues()
|
||||
cv.put("address", contact.address.toString())
|
||||
cv.put("name", contact.name)
|
||||
cv.put("status", contact.status)
|
||||
helper.getWritableDatabase.insert("contacts", null, cv)
|
||||
contactsUpdated()
|
||||
}
|
||||
|
||||
def updateContact(contact: User): Unit = {
|
||||
val cv = new ContentValues()
|
||||
cv.put("name", contact.name)
|
||||
cv.put("status", contact.status)
|
||||
helper.getWritableDatabase.update("contacts", cv, "address = ?", Array(contact.address.toString()))
|
||||
contactsUpdated()
|
||||
}
|
||||
|
||||
private def contactsUpdated(): Unit = {
|
||||
LocalBroadcastManager.getInstance(context)
|
||||
.sendBroadcast(new Intent(Database.ActionContactsUpdated))
|
||||
}
|
||||
|
||||
}
|
|
@ -3,18 +3,19 @@ package com.nutomic.ensichat.views
|
|||
import java.text.DateFormat
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import com.mobsandgeeks.adapters.{Sectionizer, SimpleSectionAdapter}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.core.Message
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
object DatesAdapter {
|
||||
|
||||
private val Sectionizer = new Sectionizer[Cursor]() {
|
||||
override def getSectionTitleForItem(item: Cursor): String = {
|
||||
private val Sectionizer = new Sectionizer[Message]() {
|
||||
override def getSectionTitleForItem(item: Message): String = {
|
||||
DateFormat
|
||||
.getDateInstance(DateFormat.MEDIUM)
|
||||
.format(Database.messageFromCursor(item).header.time.get)
|
||||
.format(item.header.time.get)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,11 +25,12 @@ object DatesAdapter {
|
|||
* Wraps [[MessagesAdapter]] and shows date between messages.
|
||||
*/
|
||||
class DatesAdapter(context: Context, messagesAdapter: MessagesAdapter)
|
||||
extends SimpleSectionAdapter[Cursor](context, messagesAdapter, R.layout.item_date, R.id.date,
|
||||
extends SimpleSectionAdapter[Message](context, messagesAdapter, R.layout.item_date, R.id.date,
|
||||
DatesAdapter.Sectionizer) {
|
||||
|
||||
def changeCursor(cursor: Cursor): Unit = {
|
||||
messagesAdapter.changeCursor(cursor)
|
||||
def replaceItems(items: Seq[Message]): Unit = {
|
||||
messagesAdapter.clear()
|
||||
messagesAdapter.addAll(items.asJava)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
package com.nutomic.ensichat.views
|
||||
|
||||
import java.text.DateFormat
|
||||
import java.util
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view._
|
||||
import android.widget._
|
||||
import com.mobsandgeeks.adapters.{InstantCursorAdapter, SimpleSectionAdapter, ViewHandler}
|
||||
import com.mobsandgeeks.adapters.{InstantAdapter, SimpleSectionAdapter, ViewHandler}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.{Address, Message}
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.views.MessagesAdapter._
|
||||
|
||||
object MessagesAdapter {
|
||||
|
||||
private def itemsAsMutableList(items: Seq[Message]): util.List[Message] = {
|
||||
val list = new util.ArrayList[Message]()
|
||||
items.foreach(list.add)
|
||||
list
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays [[Message]]s in ListView.
|
||||
|
@ -18,8 +28,9 @@ import com.nutomic.ensichat.util.Database
|
|||
* We just use the instant adapter for compatibility with [[SimpleSectionAdapter]], but don't use
|
||||
* the annotations (as it breaks separation of presentation and content).
|
||||
*/
|
||||
class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address) extends
|
||||
InstantCursorAdapter[Message](context, R.layout.item_message, classOf[Message], cursor) {
|
||||
class MessagesAdapter(context: Context, items: Seq[Message], remoteAddress: Address) extends
|
||||
InstantAdapter[Message](context, R.layout.item_message, classOf[Message],
|
||||
itemsAsMutableList(items)) {
|
||||
|
||||
private val MessagePaddingLarge = 50
|
||||
private val MessagePaddingSmall = 10
|
||||
|
@ -52,6 +63,4 @@ class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address)
|
|||
}
|
||||
})
|
||||
|
||||
override def getInstance (cursor: Cursor) = Database.messageFromCursor(cursor)
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ apply plugin: 'scala'
|
|||
|
||||
dependencies {
|
||||
compile 'org.scala-lang:scala-library:2.11.7'
|
||||
compile 'com.h2database:h2:1.4.191'
|
||||
compile 'com.typesafe.slick:slick_2.11:3.1.1'
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo}
|
|||
import com.nutomic.ensichat.core.header.ContentHeader
|
||||
import com.nutomic.ensichat.core.interfaces._
|
||||
import com.nutomic.ensichat.core.internet.InternetInterface
|
||||
import com.nutomic.ensichat.core.util.FutureHelper
|
||||
import com.nutomic.ensichat.core.util.{Database, FutureHelper}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
|
@ -16,7 +16,7 @@ import scala.concurrent.ExecutionContext.Implicits.global
|
|||
* @param maxInternetConnections Maximum number of concurrent connections that should be opened by
|
||||
* [[InternetInterface]].
|
||||
*/
|
||||
final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInterface,
|
||||
final class ConnectionHandler(settings: SettingsInterface, database: Database,
|
||||
callbacks: CallbackInterface, crypto: Crypto,
|
||||
maxInternetConnections: Int) {
|
||||
|
||||
|
@ -51,6 +51,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInt
|
|||
|
||||
def stop(): Unit = {
|
||||
transmissionInterfaces.foreach(_.destroy())
|
||||
database.close()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,4 +8,6 @@ trait CallbackInterface {
|
|||
|
||||
def onConnectionsChanged(): Unit
|
||||
|
||||
def onContactsUpdated(): Unit
|
||||
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
package com.nutomic.ensichat.core.interfaces
|
||||
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
|
||||
trait DatabaseInterface {
|
||||
|
||||
/**
|
||||
* Inserts the given new message into the database.
|
||||
*/
|
||||
def onMessageReceived(msg: Message): Unit
|
||||
|
||||
/**
|
||||
* Returns all contacts of this user.
|
||||
*/
|
||||
def getContacts: Set[User]
|
||||
|
||||
/**
|
||||
* Returns the contact with the given address if it exists.
|
||||
*/
|
||||
def getContact(address: Address): Option[User]
|
||||
|
||||
/**
|
||||
* Inserts the given device into contacts.
|
||||
*/
|
||||
def addContact(contact: User): Unit
|
||||
|
||||
def updateContact(contact: User): Unit
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package com.nutomic.ensichat.core.util
|
||||
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.header.ContentHeader
|
||||
import com.nutomic.ensichat.core.interfaces.{Log, CallbackInterface}
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
import slick.driver.H2Driver.api._
|
||||
import slick.jdbc.meta.MTable
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration.Duration
|
||||
|
||||
|
||||
/**
|
||||
* Handles persistent data storage.
|
||||
*
|
||||
* @param path The database file.
|
||||
*/
|
||||
class Database(path: File, callbackInterface: CallbackInterface) {
|
||||
|
||||
private val Tag = "Database"
|
||||
|
||||
private class Messages(tag: Tag) extends Table[Message](tag, "MESSAGES") {
|
||||
def id = primaryKey("id", (origin, messageId))
|
||||
def origin = column[String]("origin")
|
||||
def target = column[String]("target")
|
||||
def messageId = column[Long]("message_id")
|
||||
def text = column[String]("text")
|
||||
def date = column[Long]("date")
|
||||
def * = (origin, target, messageId, text, date).<> [Message, (String, String, Long, String, Long)]( { tuple =>
|
||||
val header = new ContentHeader(new Address(tuple._1),
|
||||
new Address(tuple._2),
|
||||
-1,
|
||||
Text.Type,
|
||||
Some(tuple._3),
|
||||
Some(new Date(tuple._5)))
|
||||
val body = new Text(tuple._4)
|
||||
new Message(header, body)
|
||||
}, { message =>
|
||||
Option((message.header.origin.toString(), message.header.target.toString(),
|
||||
message.header.messageId.get, message.body.asInstanceOf[Text].text,
|
||||
message.header.time.get.getTime))
|
||||
})
|
||||
}
|
||||
private val messages = TableQuery[Messages]
|
||||
|
||||
private class Contacts(tag: Tag) extends Table[User](tag, "CONTACTS") {
|
||||
def address = column[String]("address", O.PrimaryKey)
|
||||
def name = column[String]("name")
|
||||
def status = column[String]("status")
|
||||
def wrappedAddress = address.<> [Address, String](new Address(_), a => Option(a.toString()))
|
||||
def * = (wrappedAddress, name, status) <> (User.tupled, User.unapply)
|
||||
}
|
||||
private val contacts = TableQuery[Contacts]
|
||||
|
||||
private val db = Database.forURL("jdbc:h2:" + path.getAbsolutePath, driver = "org.h2.Driver")
|
||||
|
||||
// Create tables if database doesn't exist.
|
||||
{
|
||||
// H2 appends a .mv.db suffix to the path which we can't change, so we have to check that file.
|
||||
val dbFile = new File(path.getAbsolutePath + ".mv.db")
|
||||
if (!dbFile.exists()) {
|
||||
Log.i(Tag, "Database does not exist, creating tables")
|
||||
Await.result(db.run((messages.schema ++ contacts.schema).create), Duration.Inf)
|
||||
}
|
||||
}
|
||||
|
||||
def close(): Unit = {
|
||||
Await.result(db.shutdown, Duration.Inf)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given new message into the database.
|
||||
*/
|
||||
def onMessageReceived(msg: Message): Unit = {
|
||||
Await.result(db.run(messages += msg), Duration.Inf)
|
||||
}
|
||||
|
||||
def getMessages(address: Address): Seq[Message] = {
|
||||
val query = messages.filter { m =>
|
||||
m.origin === address.toString || m.target === address.toString
|
||||
}
|
||||
Await.result(db.run(query.result), Duration.Inf)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all contacts of this user.
|
||||
*/
|
||||
def getContacts: Set[User] = {
|
||||
val f = db.run(contacts.result)
|
||||
Await.result(f, Duration.Inf).toSet
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contact with the given address if it exists.
|
||||
*/
|
||||
def getContact(address: Address): Option[User] = {
|
||||
val query = contacts.filter { c =>
|
||||
c.address === address.toString
|
||||
}
|
||||
Await.result(db.run(query.result), Duration.Inf).headOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the user as a new contact.
|
||||
*/
|
||||
def addContact(contact: User): Unit = {
|
||||
Await.result(db.run(contacts += contact), Duration.Inf)
|
||||
callbackInterface.onContactsUpdated()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing contact.
|
||||
*/
|
||||
def updateContact(contact: User): Unit = {
|
||||
assert(getContact(contact.address).nonEmpty)
|
||||
Await.result(db.run(contacts.insertOrUpdate(contact)), Duration.Inf)
|
||||
callbackInterface.onContactsUpdated()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package com.nutomic.ensichat.core.util
|
||||
|
||||
import java.io.File
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.header.ContentHeader
|
||||
import com.nutomic.ensichat.core.interfaces.CallbackInterface
|
||||
import com.nutomic.ensichat.core.util.DatabaseTest._
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
import junit.framework.Assert._
|
||||
import junit.framework.TestCase
|
||||
|
||||
object DatabaseTest {
|
||||
|
||||
private val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
|
||||
private val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
|
||||
private val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
||||
private val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
||||
|
||||
private val h1 = new ContentHeader(a2, a1, -1, Text.Type, Some(123),
|
||||
Some(new GregorianCalendar(1970, 1, 1).getTime), 0)
|
||||
private val h2 = new ContentHeader(a1, a3, -1, Text.Type, Some(8765),
|
||||
Some(new GregorianCalendar(2014, 6, 10).getTime), 0)
|
||||
private val h3 = new ContentHeader(a4, a2, -1, Text.Type, Some(77),
|
||||
Some(new GregorianCalendar(2020, 11, 11).getTime), 0)
|
||||
|
||||
private val m1 = new Message(h1, new Text("first"))
|
||||
private val m2 = new Message(h2, new Text("second"))
|
||||
private val m3 = new Message(h3, new Text("third"))
|
||||
|
||||
private val u1 = new User(a1, "one", "s1")
|
||||
private val u2 = new User(a2, "two", "s2")
|
||||
private val u3 = new User(a2, "two-updated", "s2-updated")
|
||||
|
||||
}
|
||||
|
||||
class DatabaseTest extends TestCase {
|
||||
|
||||
private val databaseFile = File.createTempFile("ensichat-test", ".db")
|
||||
|
||||
private val latch = new CountDownLatch(1)
|
||||
|
||||
private val database = new Database(databaseFile, new CallbackInterface {
|
||||
override def onConnectionsChanged(): Unit = {}
|
||||
|
||||
override def onContactsUpdated(): Unit = {
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
override def onMessageReceived(msg: Message): Unit = {}
|
||||
})
|
||||
|
||||
override def tearDown(): Unit = {
|
||||
super.tearDown()
|
||||
database.close()
|
||||
databaseFile.delete()
|
||||
}
|
||||
|
||||
def testMessageSelect(): Unit = {
|
||||
database.onMessageReceived(m1)
|
||||
database.onMessageReceived(m2)
|
||||
database.onMessageReceived(m3)
|
||||
val msg = database.getMessages(a2)
|
||||
assertEquals(Seq(m1, m3), msg)
|
||||
}
|
||||
|
||||
def testAddContact(): Unit = {
|
||||
assertEquals(0, database.getContacts.size)
|
||||
database.addContact(u1)
|
||||
val contacts = database.getContacts
|
||||
assertEquals(1, contacts.size)
|
||||
assertEquals(Option(u1), database.getContact(u1.address))
|
||||
}
|
||||
|
||||
def testAddContactCallback(): Unit = {
|
||||
database.addContact(u1)
|
||||
latch.await()
|
||||
}
|
||||
|
||||
def testGetContact(): Unit = {
|
||||
assertFalse(database.getContact(u2.address).isDefined)
|
||||
database.addContact(u2)
|
||||
val c = database.getContact(u2.address)
|
||||
assertEquals(u2, c.get)
|
||||
}
|
||||
|
||||
def testUpdateContact(): Unit = {
|
||||
database.addContact(u2)
|
||||
database.updateContact(u3)
|
||||
val c = database.getContact(u2.address)
|
||||
assertEquals(u3, c.get)
|
||||
}
|
||||
|
||||
def testUpdateNonExistingContact(): Unit = {
|
||||
try {
|
||||
database.updateContact(u3)
|
||||
fail()
|
||||
} catch {
|
||||
case _: AssertionError =>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.nutomic.ensichat.server
|
||||
|
||||
import com.nutomic.ensichat.core.interfaces.DatabaseInterface
|
||||
import com.nutomic.ensichat.core.{Address, Message, User}
|
||||
|
||||
class Database extends DatabaseInterface {
|
||||
|
||||
private var contacts = Set[User]()
|
||||
|
||||
def onMessageReceived(msg: Message): Unit = {}
|
||||
|
||||
def getContacts: Set[User] = contacts
|
||||
|
||||
def getContact(address: Address): Option[User] = contacts.find(_.address == address)
|
||||
|
||||
def addContact(contact: User): Unit = contacts += contact
|
||||
|
||||
def updateContact(contact: User): Unit = {
|
||||
contacts = contacts.filterNot(_.address == contact.address)
|
||||
contacts += contact
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,7 @@ import java.util.concurrent.TimeUnit
|
|||
import com.nutomic.ensichat.core.body.Text
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
||||
import com.nutomic.ensichat.core.interfaces.{CallbackInterface, Log, SettingsInterface}
|
||||
import com.nutomic.ensichat.core.util.Database
|
||||
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto, Message}
|
||||
import scopt.OptionParser
|
||||
|
||||
|
@ -16,6 +17,7 @@ object Main extends App with CallbackInterface {
|
|||
|
||||
private val ConfigFolder = Paths.get("").toFile.getAbsoluteFile
|
||||
private val ConfigFile = new File(ConfigFolder, "config.properties")
|
||||
private val DatabaseFile = new File(ConfigFolder, "database")
|
||||
private val KeyFolder = new File(ConfigFolder, "keys")
|
||||
|
||||
private val LogInterval = TimeUnit.SECONDS.toMillis(1)
|
||||
|
@ -23,8 +25,8 @@ object Main extends App with CallbackInterface {
|
|||
private lazy val logInstance = new Logging()
|
||||
private lazy val settings = new Settings(ConfigFile)
|
||||
private lazy val crypto = new Crypto(settings, KeyFolder)
|
||||
private lazy val connectionHandler =
|
||||
new ConnectionHandler(settings, new Database(), this, crypto, 7)
|
||||
private lazy val database = new Database(DatabaseFile, this)
|
||||
private lazy val connectionHandler = new ConnectionHandler(settings, database, this, crypto, 7)
|
||||
|
||||
init()
|
||||
|
||||
|
@ -85,4 +87,6 @@ object Main extends App with CallbackInterface {
|
|||
|
||||
def onConnectionsChanged(): Unit = {}
|
||||
|
||||
def onContactsUpdated(): Unit = {}
|
||||
|
||||
}
|
||||
|
|
Reference in a new issue