diff --git a/app/.gitignore b/android/.gitignore
similarity index 100%
rename from app/.gitignore
rename to android/.gitignore
diff --git a/app/build.gradle b/android/build.gradle
similarity index 77%
rename from app/build.gradle
rename to android/build.gradle
index fa8168a..1910153 100644
--- a/app/build.gradle
+++ b/android/build.gradle
@@ -14,13 +14,18 @@ dependencies {
compile "com.android.support:appcompat-v7:23.0.0"
compile 'com.android.support:design:23.0.0'
compile 'com.android.support:multidex:1.0.1'
- androidTestCompile "com.android.support:multidex-instrumentation:1.0.1",
- { exclude module: "multidex" }
- compile "org.scala-lang:scala-library:2.11.7"
+ androidTestCompile 'com.android.support:multidex-instrumentation:1.0.1',
+ { exclude module: 'multidex' }
+ androidTestCompile project(path: ':core', configuration: 'testArtifacts')
+ compile 'org.scala-lang:scala-library:2.11.7'
compile 'com.google.guava:guava:18.0'
compile 'com.mobsandgeeks:adapter-kit:0.5.3'
+ compile project(path: ':core')
}
+// TODO: need to import core test classes
+//assembleAndroidTest.dependsOn tasks.getByPath(':core:testClasses')
+
// RtlHardcoded behaviour differs between target API versions. We only care about API 15.
preBuild.doFirst {
android.applicationVariants.each { variant ->
diff --git a/app/lint.xml b/android/lint.xml
similarity index 100%
rename from app/lint.xml
rename to android/lint.xml
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala b/android/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala
similarity index 87%
rename from app/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala
rename to android/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala
index 1e1e999..a505e8c 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala
+++ b/android/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala
@@ -6,8 +6,7 @@ import android.test.AndroidTestCase
class BluetoothInterfaceTest extends AndroidTestCase {
- private lazy val adapter = new BluetoothInterface(getContext, new Handler(), Message => Unit,
- () => Unit, Message => false)
+ private lazy val adapter = new BluetoothInterface(getContext, new Handler(), null)
/**
* Test for issue [[https://github.com/Nutomic/ensichat/issues/3 #3]].
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala b/android/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
similarity index 64%
rename from app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
rename to android/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
index 0ac6516..2b56f5f 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
+++ b/android/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
@@ -1,5 +1,6 @@
package com.nutomic.ensichat.util
+import java.util.GregorianCalendar
import java.util.concurrent.CountDownLatch
import android.content._
@@ -7,11 +8,10 @@ import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.support.v4.content.LocalBroadcastManager
import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.MessageTest._
-import com.nutomic.ensichat.protocol.body.CryptoData
-import com.nutomic.ensichat.protocol.header.ContentHeader
-import com.nutomic.ensichat.protocol.header.ContentHeaderTest._
-import com.nutomic.ensichat.protocol.{Address, Message, UserTest}
+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
@@ -22,15 +22,33 @@ object DatabaseTest {
/**
* Provides a temporary database file that can be deleted easily.
*/
- class DatabaseContext(context: Context) extends ContextWrapper(context) {
+ 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): SQLiteDatabase = {
+ 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 {
@@ -92,10 +110,10 @@ class DatabaseTest extends AndroidTestCase {
}
def testAddContact(): Unit = {
- database.addContact(UserTest.u1)
+ database.addContact(u1)
val contacts = database.getContacts
assertEquals(1, contacts.size)
- assertEquals(Option(UserTest.u1), database.getContact(UserTest.u1.address))
+ assertEquals(Option(u1), database.getContact(u1.address))
}
def testAddContactCallback(): Unit = {
@@ -105,17 +123,17 @@ class DatabaseTest extends AndroidTestCase {
override def onReceive(context: Context, intent: Intent): Unit = latch.countDown()
}
lbm.registerReceiver(receiver, new IntentFilter(Database.ActionContactsUpdated))
- database.addContact(UserTest.u1)
+ database.addContact(u1)
latch.await()
lbm.unregisterReceiver(receiver)
}
def testGetContact(): Unit = {
- database.addContact(UserTest.u2)
- assertTrue(database.getContact(UserTest.u1.address).isEmpty)
- val c = database.getContact(UserTest.u2.address)
+ database.addContact(u2)
+ assertTrue(database.getContact(u1.address).isEmpty)
+ val c = database.getContact(u2.address)
assertTrue(c.nonEmpty)
- assertEquals(Option(UserTest.u2), c)
+ assertEquals(Option(u2), c)
}
}
diff --git a/app/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
similarity index 97%
rename from app/src/main/AndroidManifest.xml
rename to android/src/main/AndroidManifest.xml
index e1122d5..2ae5228 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -51,7 +51,7 @@
android:value=".activities.MainActivity" />
-
+
diff --git a/app/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java b/android/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java
similarity index 100%
rename from app/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java
rename to android/src/main/java/com/nutomic/ensichat/util/PRNGFixes.java
diff --git a/app/src/main/res/drawable-hdpi/ic_action_send_now.png b/android/src/main/res/drawable-hdpi/ic_action_send_now.png
similarity index 100%
rename from app/src/main/res/drawable-hdpi/ic_action_send_now.png
rename to android/src/main/res/drawable-hdpi/ic_action_send_now.png
diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/android/src/main/res/drawable-hdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/drawable-hdpi/ic_launcher.png
rename to android/src/main/res/drawable-hdpi/ic_launcher.png
diff --git a/app/src/main/res/drawable-mdpi/ic_action_send_now.png b/android/src/main/res/drawable-mdpi/ic_action_send_now.png
similarity index 100%
rename from app/src/main/res/drawable-mdpi/ic_action_send_now.png
rename to android/src/main/res/drawable-mdpi/ic_action_send_now.png
diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/android/src/main/res/drawable-mdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/drawable-mdpi/ic_launcher.png
rename to android/src/main/res/drawable-mdpi/ic_launcher.png
diff --git a/app/src/main/res/drawable-xhdpi/ic_action_send_now.png b/android/src/main/res/drawable-xhdpi/ic_action_send_now.png
similarity index 100%
rename from app/src/main/res/drawable-xhdpi/ic_action_send_now.png
rename to android/src/main/res/drawable-xhdpi/ic_action_send_now.png
diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/android/src/main/res/drawable-xhdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/drawable-xhdpi/ic_launcher.png
rename to android/src/main/res/drawable-xhdpi/ic_launcher.png
diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_send_now.png b/android/src/main/res/drawable-xxhdpi/ic_action_send_now.png
similarity index 100%
rename from app/src/main/res/drawable-xxhdpi/ic_action_send_now.png
rename to android/src/main/res/drawable-xxhdpi/ic_action_send_now.png
diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/android/src/main/res/drawable-xxhdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/drawable-xxhdpi/ic_launcher.png
rename to android/src/main/res/drawable-xxhdpi/ic_launcher.png
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/android/src/main/res/drawable-xxxhdpi/ic_launcher.png
similarity index 100%
rename from app/src/main/res/drawable-xxxhdpi/ic_launcher.png
rename to android/src/main/res/drawable-xxxhdpi/ic_launcher.png
diff --git a/app/src/main/res/drawable/message_background.xml b/android/src/main/res/drawable/message_background.xml
similarity index 100%
rename from app/src/main/res/drawable/message_background.xml
rename to android/src/main/res/drawable/message_background.xml
diff --git a/app/src/main/res/layout/activity_connections.xml b/android/src/main/res/layout/activity_connections.xml
similarity index 100%
rename from app/src/main/res/layout/activity_connections.xml
rename to android/src/main/res/layout/activity_connections.xml
diff --git a/app/src/main/res/layout/activity_first_start.xml b/android/src/main/res/layout/activity_first_start.xml
similarity index 100%
rename from app/src/main/res/layout/activity_first_start.xml
rename to android/src/main/res/layout/activity_first_start.xml
diff --git a/app/src/main/res/layout/activity_main.xml b/android/src/main/res/layout/activity_main.xml
similarity index 100%
rename from app/src/main/res/layout/activity_main.xml
rename to android/src/main/res/layout/activity_main.xml
diff --git a/app/src/main/res/layout/fragment_chat.xml b/android/src/main/res/layout/fragment_chat.xml
similarity index 100%
rename from app/src/main/res/layout/fragment_chat.xml
rename to android/src/main/res/layout/fragment_chat.xml
diff --git a/app/src/main/res/layout/fragment_contacts.xml b/android/src/main/res/layout/fragment_contacts.xml
similarity index 100%
rename from app/src/main/res/layout/fragment_contacts.xml
rename to android/src/main/res/layout/fragment_contacts.xml
diff --git a/app/src/main/res/layout/fragment_identicon.xml b/android/src/main/res/layout/fragment_identicon.xml
similarity index 100%
rename from app/src/main/res/layout/fragment_identicon.xml
rename to android/src/main/res/layout/fragment_identicon.xml
diff --git a/app/src/main/res/layout/item_date.xml b/android/src/main/res/layout/item_date.xml
similarity index 100%
rename from app/src/main/res/layout/item_date.xml
rename to android/src/main/res/layout/item_date.xml
diff --git a/app/src/main/res/layout/item_message.xml b/android/src/main/res/layout/item_message.xml
similarity index 100%
rename from app/src/main/res/layout/item_message.xml
rename to android/src/main/res/layout/item_message.xml
diff --git a/app/src/main/res/layout/item_user.xml b/android/src/main/res/layout/item_user.xml
similarity index 100%
rename from app/src/main/res/layout/item_user.xml
rename to android/src/main/res/layout/item_user.xml
diff --git a/app/src/main/res/menu/main.xml b/android/src/main/res/menu/main.xml
similarity index 100%
rename from app/src/main/res/menu/main.xml
rename to android/src/main/res/menu/main.xml
diff --git a/app/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml
similarity index 100%
rename from app/src/main/res/values/colors.xml
rename to android/src/main/res/values/colors.xml
diff --git a/app/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
similarity index 100%
rename from app/src/main/res/values/strings.xml
rename to android/src/main/res/values/strings.xml
diff --git a/app/src/main/res/values/style.xml b/android/src/main/res/values/style.xml
similarity index 100%
rename from app/src/main/res/values/style.xml
rename to android/src/main/res/values/style.xml
diff --git a/app/src/main/res/xml/settings.xml b/android/src/main/res/xml/settings.xml
similarity index 84%
rename from app/src/main/res/xml/settings.xml
rename to android/src/main/res/xml/settings.xml
index 3c92659..fb153c8 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/android/src/main/res/xml/settings.xml
@@ -14,20 +14,17 @@
+ android:key="notification_sounds" />
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
similarity index 96%
rename from app/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
rename to android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
index 90e2854..936baf6 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
@@ -10,7 +10,7 @@ import android.view._
import android.widget.AdapterView.OnItemClickListener
import android.widget._
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.protocol.ChatService
+import com.nutomic.ensichat.service.CallbackHandler
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter
@@ -39,7 +39,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
list.setEmptyView(findViewById(android.R.id.empty))
val filter = new IntentFilter()
- filter.addAction(ChatService.ActionConnectionsChanged)
+ filter.addAction(CallbackHandler.ActionConnectionsChanged)
filter.addAction(Database.ActionContactsUpdated)
LocalBroadcastManager.getInstance(this)
.registerReceiver(onContactsUpdatedReceiver, filter)
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala
similarity index 90%
rename from app/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala
rename to android/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala
index 53c6c2b..e8ae5cb 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala
@@ -3,7 +3,7 @@ package com.nutomic.ensichat.activities
import android.content.{ComponentName, Context, Intent, ServiceConnection}
import android.os.{Bundle, IBinder}
import android.support.v7.app.AppCompatActivity
-import com.nutomic.ensichat.protocol.{ChatService, ChatServiceBinder}
+import com.nutomic.ensichat.service.ChatService
/**
* Connects to [[ChatService]] and provides access to it.
@@ -37,7 +37,7 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
* Clears the list containing them.
*/
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
- val binder = iBinder.asInstanceOf[ChatServiceBinder]
+ val binder = iBinder.asInstanceOf[ChatService.Binder]
chatService = Option(binder.service)
listeners.foreach(_())
listeners = Set.empty
@@ -60,6 +60,6 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
*
* Will only be set after [[runOnServiceConnected]].
*/
- def service = chatService
+ def service = chatService.map(_.getConnectionHandler)
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
similarity index 80%
rename from app/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
rename to android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
index d66367f..86905a6 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala
@@ -11,7 +11,8 @@ import android.view.{KeyEvent, View}
import android.widget.TextView.OnEditorActionListener
import android.widget.{Button, EditText, TextView}
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.fragments.SettingsFragment
+import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.core.interfaces.Settings._
/**
* Shown on first start, lets the user enter their name.
@@ -60,7 +61,7 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
override def onClick(v: View): Unit = save()
/**
- * Saves values and calls [[startMainActivity]].
+ * Saves username and default settings values, then calls [[startMainActivity]].
*/
private def save(): Unit = {
imm.hideSoftInputFromWindow(username.getWindowToken, 0)
@@ -68,8 +69,11 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
preferences
.edit()
.putBoolean(KeyIsFirstStart, false)
- .putString(SettingsFragment.KeyUserName, username.getText.toString.trim)
- .putString(SettingsFragment.KeyUserStatus, getString(R.string.default_user_status))
+ .putString(Settings.KeyUserName, username.getText.toString.trim)
+ .putString(Settings.KeyUserStatus, Settings.DefaultUserStatus)
+ .putBoolean(Settings.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
+ .putString(Settings.KeyScanInterval, DefaultScanInterval.toString)
+ .putString(Settings.KeyMaxConnections, DefaultMaxConnections.toString)
.apply()
startMainActivity()
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
similarity index 98%
rename from app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
rename to android/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
index 7dedce2..328bf8a 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala
@@ -7,8 +7,8 @@ import android.os.Bundle
import android.view.MenuItem
import android.widget.Toast
import com.nutomic.ensichat.R
+import com.nutomic.ensichat.core.Address
import com.nutomic.ensichat.fragments.{ChatFragment, ContactsFragment}
-import com.nutomic.ensichat.protocol.Address
object MainActivity {
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala
similarity index 100%
rename from app/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala
rename to android/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
similarity index 89%
rename from app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
index f09a98a..67bb71d 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala
@@ -9,10 +9,10 @@ import android.preference.PreferenceManager
import android.util.Log
import com.google.common.collect.HashBiMap
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.fragments.SettingsFragment
-import com.nutomic.ensichat.protocol.ChatService.InterfaceHandler
-import com.nutomic.ensichat.protocol._
-import com.nutomic.ensichat.protocol.body.ConnectionInfo
+import com.nutomic.ensichat.core.body.ConnectionInfo
+import com.nutomic.ensichat.core.interfaces.{Settings, TransmissionInterface}
+import com.nutomic.ensichat.core.{Address, ConnectionHandler, Message}
+import com.nutomic.ensichat.service.ChatService
import scala.collection.immutable.HashMap
@@ -29,15 +29,13 @@ object BluetoothInterface {
* Handles all Bluetooth connectivity.
*/
class BluetoothInterface(context: Context, mainHandler: Handler,
- onMessageReceived: Message => Unit, callConnectionListeners: () => Unit,
- onConnectionOpened: (Message) => Boolean)
- extends InterfaceHandler {
+ connectionHandler: ConnectionHandler) extends TransmissionInterface {
private val Tag = "BluetoothInterface"
private lazy val btAdapter = BluetoothAdapter.getDefaultAdapter
- private lazy val crypto = new Crypto(context)
+ private lazy val crypto = ChatService.newCrypto(context)
private var devices = new HashMap[Device.ID, Device]()
@@ -108,8 +106,8 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
}
val pm = PreferenceManager.getDefaultSharedPreferences(context)
- val scanInterval = pm.getString(SettingsFragment.KeyScanInterval,
- context.getResources.getString(R.string.default_scan_interval)).toInt * 1000
+ val scanInterval =
+ pm.getString(Settings.KeyScanInterval, Settings.DefaultScanInterval.toString).toInt * 1000
mainHandler.postDelayed(new Runnable {
override def run(): Unit = discover()
}, scanInterval)
@@ -175,7 +173,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
def onConnectionClosed(device: Device, socket: BluetoothSocket): Unit = {
devices -= device.id
connections -= device.id
- callConnectionListeners()
+ connectionHandler.onConnectionClosed()
addressDeviceMap.inverse().remove(device.id)
}
@@ -192,10 +190,10 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
val address = crypto.calculateAddress(info.key)
// Service.onConnectionOpened sends message, so mapping already needs to be in place.
addressDeviceMap.put(address, device)
- if (!onConnectionOpened(msg))
+ if (!connectionHandler.onConnectionOpened(msg))
addressDeviceMap.remove(address)
case _ =>
- onMessageReceived(msg)
+ connectionHandler.onMessageReceived(msg)
}
/**
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
similarity index 100%
rename from app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala
similarity index 100%
rename from app/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
similarity index 100%
rename from app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
similarity index 90%
rename from app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
index ba409fb..e033743 100644
--- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
@@ -3,12 +3,12 @@ package com.nutomic.ensichat.bluetooth
import java.io._
import android.bluetooth.{BluetoothDevice, BluetoothSocket}
-import android.content.{IntentFilter, Intent, Context, BroadcastReceiver}
+import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.util.Log
-import com.nutomic.ensichat.protocol._
-import com.nutomic.ensichat.protocol.body.ConnectionInfo
-import com.nutomic.ensichat.protocol.header.MessageHeader
-import Message.ReadMessageException
+import com.nutomic.ensichat.core.Message.ReadMessageException
+import com.nutomic.ensichat.core.body.ConnectionInfo
+import com.nutomic.ensichat.core.header.MessageHeader
+import com.nutomic.ensichat.core.{Address, Crypto, Message}
/**
* Transfers data between connnected devices.
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
similarity index 92%
rename from app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
rename to android/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
index 0845fde..54c2c1e 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
@@ -12,8 +12,9 @@ import android.widget.TextView.OnEditorActionListener
import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.EnsichatActivity
-import com.nutomic.ensichat.protocol.body.Text
-import com.nutomic.ensichat.protocol.{Address, ChatService, Message}
+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}
@@ -36,7 +37,7 @@ class ChatFragment extends ListFragment with OnClickListener {
private var address: Address = _
- private var chatService: ChatService = _
+ private var chatService: ConnectionHandler = _
private var sendButton: Button = _
@@ -93,7 +94,7 @@ class ChatFragment extends ListFragment with OnClickListener {
address = new Address(savedInstanceState.getByteArray("address"))
LocalBroadcastManager.getInstance(getActivity)
- .registerReceiver(onMessageReceivedReceiver, new IntentFilter(ChatService.ActionMessageReceived))
+ .registerReceiver(onMessageReceivedReceiver, new IntentFilter(CallbackHandler.ActionMessageReceived))
}
override def onSaveInstanceState(outState: Bundle): Unit = {
@@ -124,7 +125,7 @@ class ChatFragment extends ListFragment with OnClickListener {
*/
private val onMessageReceivedReceiver = new BroadcastReceiver {
override def onReceive(context: Context, intent: Intent): Unit = {
- val msg = intent.getSerializableExtra(ChatService.ExtraMessage).asInstanceOf[Message]
+ val msg = intent.getSerializableExtra(CallbackHandler.ExtraMessage).asInstanceOf[Message]
if (!Set(msg.header.origin, msg.header.target).contains(address))
return
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
similarity index 93%
rename from app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
rename to android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
index e3f9294..bb665cc 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala
@@ -2,9 +2,8 @@ package com.nutomic.ensichat.fragments
import java.io.File
-import android.app.{ActionBar, ListFragment}
+import android.app.ListFragment
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
-import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.preference.PreferenceManager
@@ -12,10 +11,11 @@ import android.support.v4.content.{ContextCompat, LocalBroadcastManager}
import android.support.v7.widget.Toolbar
import android.view.View.OnClickListener
import android.view._
-import android.widget.{ListView, TextView, Toast}
+import android.widget.{ListView, TextView}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
-import com.nutomic.ensichat.protocol.{ChatService, Crypto}
+import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.service.{CallbackHandler, ChatService}
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter
@@ -43,7 +43,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
setListAdapter(adapter)
setHasOptionsMenu(true)
lbm.registerReceiver(onContactsUpdatedListener, new IntentFilter(Database.ActionContactsUpdated))
- lbm.registerReceiver(onConnectionsChangedListener, new IntentFilter(ChatService.ActionConnectionsChanged))
+ lbm.registerReceiver(onConnectionsChangedListener, new IntentFilter(CallbackHandler.ActionConnectionsChanged))
}
override def onResume(): Unit = {
@@ -97,9 +97,9 @@ class ContactsFragment extends ListFragment with OnClickListener {
val fragment = new IdenticonFragment()
val bundle = new Bundle()
bundle.putString(
- IdenticonFragment.ExtraAddress, new Crypto(getActivity).localAddress.toString)
+ IdenticonFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString)
bundle.putString(
- IdenticonFragment.ExtraUserName, prefs.getString(SettingsFragment.KeyUserName, ""))
+ IdenticonFragment.ExtraUserName, prefs.getString(Settings.KeyUserName, ""))
fragment.setArguments(bundle)
fragment.show(getFragmentManager, "dialog")
true
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/IdenticonFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/IdenticonFragment.scala
similarity index 92%
rename from app/src/main/scala/com/nutomic/ensichat/fragments/IdenticonFragment.scala
rename to android/src/main/scala/com/nutomic/ensichat/fragments/IdenticonFragment.scala
index bb3ca4a..5dff6b5 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/IdenticonFragment.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/fragments/IdenticonFragment.scala
@@ -2,10 +2,10 @@ package com.nutomic.ensichat.fragments
import android.app.{AlertDialog, Dialog, DialogFragment}
import android.os.Bundle
-import android.view.{LayoutInflater, View, ViewGroup}
+import android.view.LayoutInflater
import android.widget.{ImageView, TextView}
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.protocol.Address
+import com.nutomic.ensichat.core.Address
import com.nutomic.ensichat.util.IdenticonGenerator
object IdenticonFragment {
diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
similarity index 53%
rename from app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
rename to android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
index b39ef02..7c5aed4 100644
--- a/app/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala
@@ -5,35 +5,26 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.preference.Preference.OnPreferenceChangeListener
import android.preference.{Preference, PreferenceFragment, PreferenceManager}
-import com.nutomic.ensichat.activities.EnsichatActivity
-import com.nutomic.ensichat.fragments.SettingsFragment._
-import com.nutomic.ensichat.protocol.body.UserInfo
-import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.{BuildConfig, R}
+import com.nutomic.ensichat.activities.EnsichatActivity
+import com.nutomic.ensichat.core.body.UserInfo
+import com.nutomic.ensichat.core.interfaces.Settings._
+import com.nutomic.ensichat.fragments.SettingsFragment._
+import com.nutomic.ensichat.util.Database
object SettingsFragment {
-
- val KeyUserName = "user_name"
- val KeyUserStatus = "user_status"
- val KeyScanInterval = "scan_interval_seconds"
- val MaxConnections = "max_connections"
- val Version = "version"
-
+ val Version = "version"
}
/**
* Settings screen.
*/
-class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListener
- with OnSharedPreferenceChangeListener {
+class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
private lazy val database = new Database(getActivity)
- private lazy val name = findPreference(KeyUserName)
- private lazy val status = findPreference(KeyUserStatus)
- private lazy val scanInterval = findPreference(KeyScanInterval)
- private lazy val maxConnections = findPreference(MaxConnections)
- private lazy val version = findPreference(Version)
+ private lazy val maxConnections = findPreference(KeyMaxConnections)
+ private lazy val version = findPreference(Version)
private lazy val prefs = PreferenceManager.getDefaultSharedPreferences(getActivity)
@@ -42,20 +33,7 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
addPreferencesFromResource(R.xml.settings)
- name.setSummary(prefs.getString(KeyUserName, ""))
- name.setOnPreferenceChangeListener(this)
- status.setSummary(prefs.getString(KeyUserStatus, ""))
- status.setOnPreferenceChangeListener(this)
-
- scanInterval.setOnPreferenceChangeListener(this)
- scanInterval.setSummary(prefs.getString(
- KeyScanInterval, getResources.getString(R.string.default_scan_interval)))
-
- if (BuildConfig.DEBUG) {
- maxConnections.setOnPreferenceChangeListener(this)
- maxConnections.setSummary(prefs.getString(
- MaxConnections, getResources.getString(R.string.default_max_connections)))
- } else
+ if (!BuildConfig.DEBUG)
getPreferenceScreen.removePreference(maxConnections)
val packageInfo = getActivity.getPackageManager.getPackageInfo(getActivity.getPackageName, 0)
@@ -68,14 +46,6 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
prefs.unregisterOnSharedPreferenceChangeListener(this)
}
- /**
- * Updates summary, sends updated name to contacts.
- */
- override def onPreferenceChange(preference: Preference, newValue: AnyRef): Boolean = {
- preference.setSummary(newValue.toString)
- true
- }
-
/**
* Sends the updated username or status to all contacts.
*/
@@ -85,6 +55,7 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
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))
+ case _ =>
}
}
diff --git a/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala b/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala
new file mode 100644
index 0000000..8b570d7
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala
@@ -0,0 +1,39 @@
+package com.nutomic.ensichat.service
+
+import android.content.{Context, Intent}
+import android.support.v4.content.LocalBroadcastManager
+import com.nutomic.ensichat.core.{ConnectionHandler, Message}
+import com.nutomic.ensichat.core.interfaces.CallbackInterface
+import com.nutomic.ensichat.service.CallbackHandler._
+
+object CallbackHandler {
+
+ val ActionMessageReceived = "message_received"
+ val ActionConnectionsChanged = "connections_changed"
+
+ val ExtraMessage = "extra_message"
+
+}
+
+/**
+ * Receives events from [[ConnectionHandler]] and sends them as local broadcasts.
+ */
+class CallbackHandler(context: Context, notificationHandler: NotificationHandler)
+ extends CallbackInterface {
+
+ def onMessageReceived(msg: Message): Unit = {
+ notificationHandler.onMessageReceived(msg)
+ val i = new Intent(ActionMessageReceived)
+ i.putExtra(ExtraMessage, msg)
+ LocalBroadcastManager.getInstance(context)
+ .sendBroadcast(i)
+
+ }
+
+ def onConnectionsChanged(): Unit = {
+ val i = new Intent(ActionConnectionsChanged)
+ LocalBroadcastManager.getInstance(context)
+ .sendBroadcast(i)
+ }
+
+}
diff --git a/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala b/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala
new file mode 100644
index 0000000..92da36c
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala
@@ -0,0 +1,58 @@
+package com.nutomic.ensichat.service
+
+import java.io.File
+
+import android.app.Service
+import android.content.{Context, Intent}
+import android.os.Handler
+import com.nutomic.ensichat.bluetooth.BluetoothInterface
+import com.nutomic.ensichat.core.interfaces.Log
+import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
+import com.nutomic.ensichat.util.{Database, PRNGFixes, SettingsWrapper}
+
+object ChatService {
+
+ case class Binder(service: ChatService) extends android.os.Binder
+
+ private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
+ def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
+
+}
+
+class ChatService extends Service {
+
+ private lazy val binder = new ChatService.Binder(this)
+
+ private lazy val notificationHandler = new NotificationHandler(this)
+
+ private val callbackHandler = new CallbackHandler(this, notificationHandler)
+
+ private lazy val connectionHandler =
+ new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
+ ChatService.keyFolder(this))
+
+ override def onBind(intent: Intent) = binder
+
+ override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
+
+ /**
+ * Generates keys and starts Bluetooth interface.
+ */
+ override def onCreate(): Unit = {
+ super.onCreate()
+ PRNGFixes.apply()
+ Log.setLogClass(classOf[android.util.Log])
+ notificationHandler.showPersistentNotification()
+ connectionHandler.start()
+ connectionHandler.setTransmissionInterface(new BluetoothInterface(this, new Handler(),
+ connectionHandler))
+ }
+
+ override def onDestroy(): Unit = {
+ notificationHandler.cancelPersistentNotification()
+ connectionHandler.stop()
+ }
+
+ def getConnectionHandler = connectionHandler
+
+}
\ No newline at end of file
diff --git a/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala b/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala
new file mode 100644
index 0000000..8210e06
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala
@@ -0,0 +1,74 @@
+package com.nutomic.ensichat.service
+
+import android.app.{Notification, NotificationManager, PendingIntent}
+import android.content.{Context, Intent}
+import android.preference.PreferenceManager
+import android.support.v4.app.NotificationCompat
+import com.nutomic.ensichat.R
+import com.nutomic.ensichat.activities.MainActivity
+import com.nutomic.ensichat.core.Message
+import com.nutomic.ensichat.core.body.Text
+import com.nutomic.ensichat.core.interfaces.Settings
+import com.nutomic.ensichat.service.NotificationHandler._
+
+object NotificationHandler {
+
+ private val NotificationIdRunning = 1
+
+ private val NotificationIdNewMessage = 2
+
+}
+
+/**
+ * Displays notifications for new messages and while the app is running.
+ */
+class NotificationHandler(context: Context) {
+
+ private lazy val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
+ .asInstanceOf[NotificationManager]
+
+ def showPersistentNotification(): Unit = {
+ val intent = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
+ val notification = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.ic_launcher)
+ .setContentTitle(context.getString(R.string.app_name))
+ .setContentIntent(intent)
+ .setOngoing(true)
+ .setPriority(Notification.PRIORITY_MIN)
+ .build()
+ notificationManager.notify(NotificationIdRunning, notification)
+ }
+
+ def cancelPersistentNotification() = notificationManager.cancel(NotificationIdRunning)
+
+ def onMessageReceived(msg: Message): Unit = msg.body match {
+ case text: Text =>
+ if (msg.header.origin == ChatService.newCrypto(context).localAddress)
+ return
+
+ val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
+ val notification = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.ic_launcher)
+ .setContentTitle(context.getString(R.string.notification_message))
+ .setContentText(text.text)
+ .setDefaults(defaults())
+ .setContentIntent(pi)
+ .setAutoCancel(true)
+ .build()
+
+ notificationManager.notify(NotificationIdNewMessage, notification)
+ case _ =>
+ }
+
+ /**
+ * Returns the default notification options that should be used.
+ */
+ private def defaults(): Int = {
+ val sp = PreferenceManager.getDefaultSharedPreferences(context)
+ if (sp.getBoolean(Settings.KeyNotificationSoundsOn, Settings.DefaultNotificationSoundsOn))
+ Notification.DEFAULT_ALL
+ else
+ Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
+ }
+
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala b/android/src/main/scala/com/nutomic/ensichat/util/Database.scala
similarity index 70%
rename from app/src/main/scala/com/nutomic/ensichat/util/Database.scala
rename to android/src/main/scala/com/nutomic/ensichat/util/Database.scala
index a50cfbc..2044e63 100644
--- a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/util/Database.scala
@@ -6,12 +6,10 @@ 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.protocol._
-import com.nutomic.ensichat.protocol.body.Text
-import com.nutomic.ensichat.protocol.header.ContentHeader
-
-import scala.collection.SortedSet
-import scala.collection.immutable.TreeSet
+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 {
@@ -55,24 +53,38 @@ object Database {
/**
* Stores all messages and contacts in SQL database.
*/
-class Database(context: Context)
- extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) {
+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)
+ }
+ }
- override def onCreate(db: SQLiteDatabase): Unit = {
- db.execSQL(Database.CreateContactsTable)
- db.execSQL(Database.CreateMessagesTable)
}
+ private val helper = new Helper()
+
+ def close() = helper.close()
+
def getMessagesCursor(address: Address): Cursor = {
- getReadableDatabase.query(true,
+ 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)
}
- /**
- * Inserts the given new message into the database.
- */
def onMessageReceived(msg: Message): Unit = msg.body match {
case text: Text =>
val cv = new ContentValues()
@@ -82,14 +94,11 @@ class Database(context: Context)
cv.put("message_id", msg.header.messageId.get.toString)
cv.put("date", msg.header.time.get.getTime.toString)
cv.put("text", text.text)
- getWritableDatabase.insert("messages", null, cv)
+ helper.getWritableDatabase.insert("messages", null, cv)
}
- /**
- * Returns all contacts of this user.
- */
def getContacts: Set[User] = {
- val c = getReadableDatabase.query(true, "contacts", Array("address", "name", "status"), "", Array(),
+ val c = helper.getReadableDatabase.query(true, "contacts", Array("address", "name", "status"), "", Array(),
null, null, null, null)
var contacts = Set[User]()
while (c.moveToNext()) {
@@ -101,12 +110,9 @@ class Database(context: Context)
contacts
}
- /**
- * Returns the contact with the given address if it exists.
- */
def getContact(address: Address): Option[User] = {
- val c = getReadableDatabase.query(true, "contacts", Array("address", "name", "status"), "address = ?",
- Array(address.toString), null, null, null, null)
+ 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"))),
@@ -120,15 +126,12 @@ class Database(context: Context)
}
}
- /**
- * Inserts the given device into contacts.
- */
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)
- getWritableDatabase.insert("contacts", null, cv)
+ helper.getWritableDatabase.insert("contacts", null, cv)
contactsUpdated()
}
@@ -136,7 +139,7 @@ class Database(context: Context)
val cv = new ContentValues()
cv.put("name", contact.name)
cv.put("status", contact.status)
- getWritableDatabase.update("contacts", cv, "address = ?", Array(contact.address.toString))
+ helper.getWritableDatabase.update("contacts", cv, "address = ?", Array(contact.address.toString))
contactsUpdated()
}
@@ -145,13 +148,4 @@ class Database(context: Context)
.sendBroadcast(new Intent(Database.ActionContactsUpdated))
}
- 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)
- }
- }
-
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala b/android/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala
similarity index 97%
rename from app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala
rename to android/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala
index 4039a5c..cf65962 100644
--- a/app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala
@@ -3,7 +3,7 @@ package com.nutomic.ensichat.util
import android.content.Context
import android.graphics.Bitmap.Config
import android.graphics.{Bitmap, Canvas, Color}
-import com.nutomic.ensichat.protocol.Address
+import com.nutomic.ensichat.core.Address
/**
* Calculates a unique identicon for the given hash.
diff --git a/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala b/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala
new file mode 100644
index 0000000..fc8bd8e
--- /dev/null
+++ b/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala
@@ -0,0 +1,23 @@
+package com.nutomic.ensichat.util
+
+import android.content.Context
+import android.preference.PreferenceManager
+import com.nutomic.ensichat.core.interfaces.Settings
+
+class SettingsWrapper(context: Context) extends Settings {
+
+ private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+
+ override def get[T](key: String, default: T): T = default match {
+ case s: String => prefs.getString(key, s).asInstanceOf[T]
+ case i: Int => prefs.getInt(key, i).asInstanceOf[T]
+ case l: Long => prefs.getLong(key, l).asInstanceOf[T]
+ }
+
+ override def put[T](key: String, value: T): Unit = value match {
+ case s: String => prefs.edit().putString(key, s).apply()
+ case i: Int => prefs.edit().putInt(key, i).apply()
+ case l: Long => prefs.edit().putLong(key, l).apply()
+ }
+
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala b/android/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala
similarity index 88%
rename from app/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala
rename to android/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala
index b0f143c..d852579 100644
--- a/app/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala
@@ -1,14 +1,11 @@
package com.nutomic.ensichat.views
import java.text.DateFormat
-import java.util.Date
import android.content.Context
import android.database.Cursor
import com.mobsandgeeks.adapters.{Sectionizer, SimpleSectionAdapter}
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.protocol.Message
-import com.nutomic.ensichat.protocol.header.ContentHeader
import com.nutomic.ensichat.util.Database
object DatesAdapter {
diff --git a/app/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala b/android/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala
similarity index 95%
rename from app/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala
rename to android/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala
index 5c55b4d..dacc0de 100644
--- a/app/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala
@@ -8,8 +8,8 @@ import android.view._
import android.widget._
import com.mobsandgeeks.adapters.{InstantCursorAdapter, SimpleSectionAdapter, ViewHandler}
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.protocol.body.Text
-import com.nutomic.ensichat.protocol.{Address, Message}
+import com.nutomic.ensichat.core.body.Text
+import com.nutomic.ensichat.core.{Address, Message}
import com.nutomic.ensichat.util.Database
/**
diff --git a/app/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala b/android/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala
similarity index 97%
rename from app/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala
rename to android/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala
index a1e93c4..a2d8036 100644
--- a/app/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala
+++ b/android/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala
@@ -7,8 +7,8 @@ import android.view.View.OnClickListener
import android.view.{LayoutInflater, View, ViewGroup}
import android.widget.{ArrayAdapter, ImageView, TextView}
import com.nutomic.ensichat.R
+import com.nutomic.ensichat.core.User
import com.nutomic.ensichat.fragments.IdenticonFragment
-import com.nutomic.ensichat.protocol.{Crypto, User}
import com.nutomic.ensichat.util.IdenticonGenerator
/**
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/activities/MainActivityTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/activities/MainActivityTest.scala
deleted file mode 100644
index 6c37a5d..0000000
--- a/app/src/androidTest/scala/com/nutomic/ensichat/activities/MainActivityTest.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.nutomic.ensichat.activities
-
-import android.bluetooth.BluetoothAdapter
-import android.content._
-import android.test.ActivityUnitTestCase
-import junit.framework.Assert
-
-class MainActivityTest extends ActivityUnitTestCase[MainActivity](classOf[MainActivity]) {
-
- var lastIntent: Intent = _
-
- class ActivityContextWrapper(context: Context) extends ContextWrapper(context) {
- override def startService(service: Intent): ComponentName = {
- lastIntent = service
- null
- }
-
- override def stopService(name: Intent): Boolean = {
- lastIntent = name
- true
- }
-
- override def bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean = false
-
- override def unbindService(conn: ServiceConnection): Unit = {}
- }
-
- override def setUp(): Unit = {
- setActivityContext(new ActivityContextWrapper(getInstrumentation.getTargetContext))
- startActivity(new Intent(), null, null)
- }
-
- def testRequestBluetoothDiscoverable(): Unit = {
- val intent: Intent = getStartedActivityIntent
- Assert.assertEquals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE, intent.getAction)
- Assert.assertEquals(0, intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, -1))
- }
-
-}
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala
deleted file mode 100644
index b574628..0000000
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.nutomic.ensichat.protocol
-
-import android.test.AndroidTestCase
-import junit.framework.Assert._
-
-class CryptoTest extends AndroidTestCase {
-
- private lazy val crypto: Crypto = new Crypto(getContext)
-
- override def setUp(): Unit = {
- super.setUp()
- if (!crypto.localKeysExist) {
- crypto.generateLocalKeys()
- }
- }
-
- def testSignVerify(): Unit = {
- MessageTest.messages.foreach { m =>
- val signed = crypto.sign(m)
- assertTrue(crypto.verify(signed, crypto.getLocalPublicKey))
- assertEquals(m.header, signed.header)
- assertEquals(m.body, signed.body)
- }
- }
-
- def testEncryptDecrypt(): Unit = {
- MessageTest.messages.foreach{ m =>
- val encrypted = crypto.encrypt(crypto.sign(m), crypto.getLocalPublicKey)
- val decrypted = crypto.decrypt(encrypted)
- assertEquals(m.body, decrypted.body)
- assertEquals(m.header, encrypted.header)
- }
- }
-
-}
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/body/ConnectionInfoTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/body/ConnectionInfoTest.scala
deleted file mode 100644
index 1cf5550..0000000
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/body/ConnectionInfoTest.scala
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.nutomic.ensichat.protocol.body
-
-import android.content.Context
-import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.Crypto
-import junit.framework.Assert
-
-object ConnectionInfoTest {
-
- def generateCi(context: Context) = {
- val crypto = new Crypto(context)
- if (!crypto.localKeysExist)
- crypto.generateLocalKeys()
- new ConnectionInfo(crypto.getLocalPublicKey)
- }
-
-}
-
-class ConnectionInfoTest extends AndroidTestCase {
-
- def testWriteRead(): Unit = {
- val ci = ConnectionInfoTest.generateCi(getContext)
- val bytes = ci.write
- val body = ConnectionInfo.read(bytes)
- Assert.assertEquals(ci.key, body.asInstanceOf[ConnectionInfo].key)
- }
-
-}
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/body/UserInfoTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/body/UserInfoTest.scala
deleted file mode 100644
index 35efa89..0000000
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/body/UserInfoTest.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.nutomic.ensichat.protocol.body
-
-import android.test.AndroidTestCase
-import junit.framework.Assert
-
-class UserInfoTest extends AndroidTestCase {
-
- def testWriteRead(): Unit = {
- val name = new UserInfo("name", "status")
- val bytes = name.write
- val body = UserInfo.read(bytes)
- Assert.assertEquals(name, body.asInstanceOf[UserInfo])
- }
-
-}
diff --git a/app/src/main/res/values/settings_defaults.xml b/app/src/main/res/values/settings_defaults.xml
deleted file mode 100644
index 35db639..0000000
--- a/app/src/main/res/values/settings_defaults.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Let\'s chat!
-
- 15
-
- true
-
- 1000000
-
-
\ No newline at end of file
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
deleted file mode 100644
index 8070b88..0000000
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
+++ /dev/null
@@ -1,231 +0,0 @@
-package com.nutomic.ensichat.protocol
-
-import java.util.Date
-
-import android.app.{Notification, NotificationManager, PendingIntent, Service}
-import android.content.{Context, Intent}
-import android.os.Handler
-import android.preference.PreferenceManager
-import android.support.v4.app.NotificationCompat
-import android.support.v4.content.LocalBroadcastManager
-import android.util.Log
-import com.nutomic.ensichat.R
-import com.nutomic.ensichat.activities.MainActivity
-import com.nutomic.ensichat.bluetooth.BluetoothInterface
-import com.nutomic.ensichat.fragments.SettingsFragment
-import com.nutomic.ensichat.protocol.body.{ConnectionInfo, MessageBody, UserInfo}
-import com.nutomic.ensichat.protocol.header.ContentHeader
-import com.nutomic.ensichat.util.{Database, FutureHelper, NotificationHandler}
-
-import scala.concurrent.ExecutionContext.Implicits.global
-
-object ChatService {
-
- val ActionStopService = "stop_service"
- val ActionMessageReceived = "message_received"
- val ActionConnectionsChanged = "connections_changed"
-
- val ExtraMessage = "extra_message"
-
- abstract class InterfaceHandler {
-
- def create(): Unit
-
- def destroy(): Unit
-
- def send(nextHop: Address, msg: Message): Unit
-
- }
-
-}
-
-/**
- * High-level handling of all message transfers and callbacks.
- */
-class ChatService extends Service {
-
- private val Tag = "ChatService"
-
- private lazy val database = new Database(this)
-
- private lazy val preferences = PreferenceManager.getDefaultSharedPreferences(this)
-
- private val mainHandler = new Handler()
-
- private lazy val binder = new ChatServiceBinder(this)
-
- private lazy val crypto = new Crypto(this)
-
- private lazy val btInterface = new BluetoothInterface(this, mainHandler,
- onMessageReceived, callConnectionListeners, onConnectionOpened)
-
- private lazy val notificationHandler = new NotificationHandler(this)
-
- private lazy val router = new Router(connections, sendVia)
-
- private lazy val seqNumGenerator = new SeqNumGenerator(this)
-
- private lazy val notificationManager =
- getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
-
- /**
- * Holds all known users.
- *
- * This is for user names that were received during runtime, and is not persistent.
- */
- private var knownUsers = Set[User]()
-
- /**
- * Generates keys and starts Bluetooth interface.
- */
- override def onCreate(): Unit = {
- super.onCreate()
-
- showPersistentNotification()
-
- FutureHelper {
- crypto.generateLocalKeys()
-
- btInterface.create()
- Log.i(Tag, "Service started, address is " + crypto.localAddress)
- }
- }
-
- def showPersistentNotification(): Unit = {
- val openIntent = PendingIntent.getActivity(this, 0, new Intent(this, classOf[MainActivity]), 0)
- val notification = new NotificationCompat.Builder(this)
- .setSmallIcon(R.drawable.ic_launcher)
- .setContentTitle(getString(R.string.app_name))
- .setContentIntent(openIntent)
- .setOngoing(true)
- .setPriority(Notification.PRIORITY_MIN)
- .build()
- notificationManager.notify(NotificationHandler.NotificationIdRunning, notification)
- }
-
- override def onDestroy(): Unit = {
- notificationManager.cancel(NotificationHandler.NotificationIdRunning)
- btInterface.destroy()
- }
-
- override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
-
- override def onBind(intent: Intent) = binder
-
- /**
- * Sends a new message to the given target address.
- */
- def sendTo(target: Address, body: MessageBody): Unit = {
- FutureHelper {
- val messageId = preferences.getLong("message_id", 0)
- val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(),
- body.contentType, Some(messageId), Some(new Date()))
- preferences.edit().putLong("message_id", messageId + 1)
-
- val msg = new Message(header, body)
- val encrypted = crypto.encrypt(crypto.sign(msg))
- router.onReceive(encrypted)
- onNewMessage(msg)
- }
- }
-
- private def sendVia(nextHop: Address, msg: Message) =
- btInterface.send(nextHop, msg)
-
- /**
- * Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]].
- */
- def onMessageReceived(msg: Message): Unit = {
- if (msg.header.target == crypto.localAddress) {
- val decrypted = crypto.decrypt(msg)
- if (!crypto.verify(decrypted)) {
- Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
- return
- }
- onNewMessage(decrypted)
- } else {
- router.onReceive(msg)
- }
- }
-
- /**
- * Handles all (locally and remotely sent) new messages.
- */
- private def onNewMessage(msg: Message): Unit = msg.body match {
- case ui: UserInfo =>
- val contact = new User(msg.header.origin, ui.name, ui.status)
- knownUsers += contact
- if (database.getContact(msg.header.origin).nonEmpty)
- database.updateContact(contact)
-
- callConnectionListeners()
- case _ =>
- val origin = msg.header.origin
- if (origin != crypto.localAddress && database.getContact(origin).isEmpty)
- database.addContact(getUser(origin))
-
- database.onMessageReceived(msg)
- notificationHandler.onMessageReceived(msg)
- val i = new Intent(ChatService.ActionMessageReceived)
- i.putExtra(ChatService.ExtraMessage, msg)
- LocalBroadcastManager.getInstance(this)
- .sendBroadcast(i)
- }
-
- /**
- * Opens connection to a direct neighbor.
- *
- * This adds the other node's public key if we don't have it. If we do, it validates the signature
- * with the stored key.
- *
- * The caller must invoke [[callConnectionListeners()]]
- *
- * @param msg The message containing [[ConnectionInfo]] to open the connection.
- * @return True if the connection is valid
- */
- def onConnectionOpened(msg: Message): Boolean = {
- val maxConnections = preferences.getString(SettingsFragment.MaxConnections,
- getResources.getString(R.string.default_max_connections)).toInt
- if (connections().size == maxConnections) {
- Log.i(Tag, "Maximum number of connections reached")
- return false
- }
-
- 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)
- return false
- }
-
- if (crypto.havePublicKey(sender) && !crypto.verify(msg, crypto.getPublicKey(sender))) {
- Log.i(Tag, "Ignoring ConnectionInfo message with invalid signature")
- return false
- }
-
- synchronized {
- if (!crypto.havePublicKey(sender)) {
- crypto.addPublicKey(sender, info.key)
- Log.i(Tag, "Added public key for new device " + sender.toString)
- }
- }
-
- Log.i(Tag, "Node " + sender + " connected")
- sendTo(sender, new UserInfo(preferences.getString(SettingsFragment.KeyUserName, ""),
- preferences.getString(SettingsFragment.KeyUserStatus, "")))
- callConnectionListeners()
- true
- }
-
- def callConnectionListeners(): Unit = {
- LocalBroadcastManager.getInstance(this)
- .sendBroadcast(new Intent(ChatService.ActionConnectionsChanged))
- }
-
- def connections() =
- btInterface.getConnections
-
- def getUser(address: Address) =
- knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, ""))
-
-}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatServiceBinder.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatServiceBinder.scala
deleted file mode 100644
index 2840485..0000000
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatServiceBinder.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.nutomic.ensichat.protocol
-
-import android.os.Binder
-
-case class ChatServiceBinder (service: ChatService) extends Binder
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala
deleted file mode 100644
index 9b58209..0000000
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.nutomic.ensichat.protocol
-
-import android.content.Context
-import android.preference.PreferenceManager
-import com.nutomic.ensichat.protocol.header.ContentHeader
-
-/**
- * Generates sequence numbers acorrding to protocol, which are stored persistently.
- */
-class SeqNumGenerator(context: Context) {
-
- private val KeySequenceNumber = "sequence_number"
-
- private val pm = PreferenceManager.getDefaultSharedPreferences(context)
-
- private var current = pm.getInt(KeySequenceNumber, ContentHeader.SeqNumRange.head)
-
- def next(): Int = {
- current += 1
- pm.edit().putInt(KeySequenceNumber, current)
- current
- }
-
-}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/User.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/User.scala
deleted file mode 100644
index 714af4a..0000000
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/User.scala
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.nutomic.ensichat.protocol
-
-case class User(address: Address, name: String, status: String)
\ No newline at end of file
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala b/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala
deleted file mode 100644
index 3511156..0000000
--- a/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.nutomic.ensichat.util
-
-import android.app.{Notification, NotificationManager, PendingIntent}
-import android.content.{Context, Intent}
-import android.preference.PreferenceManager
-import android.support.v4.app.NotificationCompat
-import com.nutomic.ensichat.R
-import com.nutomic.ensichat.activities.MainActivity
-import com.nutomic.ensichat.protocol.body.Text
-import com.nutomic.ensichat.protocol.{Crypto, Message}
-
-object NotificationHandler {
-
- val NotificationIdRunning = 1
-
- val NotificationIdNewMessage = 2
-
-}
-
-/**
- * Displays notifications for new messages.
- */
-class NotificationHandler(context: Context) {
-
- def onMessageReceived(msg: Message): Unit = msg.body match {
- case text: Text =>
- if (msg.header.origin == new Crypto(context).localAddress)
- return
-
- val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
- val notification = new NotificationCompat.Builder(context)
- .setSmallIcon(R.drawable.ic_launcher)
- .setContentTitle(context.getString(R.string.notification_message))
- .setContentText(text.text)
- .setDefaults(defaults())
- .setContentIntent(pi)
- .setAutoCancel(true)
- .build()
-
- val nm = context.getSystemService(Context.NOTIFICATION_SERVICE)
- .asInstanceOf[NotificationManager]
- nm.notify(NotificationHandler.NotificationIdNewMessage, notification)
- case _ =>
- }
-
- /**
- * Returns the default notification options that should be used.
- */
- def defaults(): Int = {
- val sp = PreferenceManager.getDefaultSharedPreferences(context)
- val defaultSounds = context.getResources.getBoolean(R.bool.default_notification_sounds)
- if (sp.getBoolean("notification_sounds", defaultSounds))
- Notification.DEFAULT_ALL
- else
- Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
- }
-
-}
diff --git a/build.gradle b/build.gradle
index c5e32b1..0ac515d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,4 +1,3 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply plugin: 'com.github.ben-manes.versions'
buildscript {
@@ -8,14 +7,12 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.11.3'
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
+ mavenCentral()
}
}
diff --git a/core/.gitignore b/core/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/core/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/core/build.gradle b/core/build.gradle
new file mode 100644
index 0000000..2d6730e
--- /dev/null
+++ b/core/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'scala'
+
+dependencies {
+ compile 'org.scala-lang:scala-library:2.11.7'
+ testCompile 'junit:junit:4.12'
+ testCompile 'commons-io:commons-io:2.4'
+}
+
+test {
+ systemProperty "testDir", new File(buildDir, "/test/").toString()
+}
+
+task myTestsJar(type: Jar) {
+ from sourceSets.test.output, sourceSets.main.output
+}
+
+configurations {
+ testArtifacts
+}
+
+artifacts {
+ testArtifacts myTestsJar
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Address.scala b/core/src/main/scala/com/nutomic/ensichat/core/Address.scala
similarity index 92%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/Address.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/Address.scala
index b13992a..59ba756 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/Address.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Address.scala
@@ -1,6 +1,4 @@
-package com.nutomic.ensichat.protocol
-
-import java.util
+package com.nutomic.ensichat.core
object Address {
@@ -41,7 +39,7 @@ case class Address(bytes: Array[Byte]) {
.toArray)
}
- override def hashCode = util.Arrays.hashCode(bytes)
+ override def hashCode = java.util.Arrays.hashCode(bytes)
override def equals(a: Any) = a match {
case o: Address => bytes.deep == o.bytes.deep
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala b/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala
new file mode 100644
index 0000000..6bd16fb
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala
@@ -0,0 +1,160 @@
+package com.nutomic.ensichat.core
+
+import java.io.File
+import java.util.Date
+
+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.util.FutureHelper
+
+import scala.concurrent.ExecutionContext.Implicits.global
+
+/**
+ * High-level handling of all message transfers and callbacks.
+ */
+class ConnectionHandler(settings: Settings, database: DatabaseInterface,
+ callbacks: CallbackInterface, keyFolder: File) {
+
+ private val Tag = "ConnectionHandler"
+
+ private lazy val crypto = new Crypto(settings, keyFolder)
+
+ private var transmissionInterface: TransmissionInterface = _
+
+ private lazy val router = new Router(connections, sendVia)
+
+ private lazy val seqNumGenerator = new SeqNumGenerator(settings)
+
+ /**
+ * Holds all known users.
+ *
+ * This is for user names that were received during runtime, and is not persistent.
+ */
+ private var knownUsers = Set[User]()
+
+ /**
+ * Generates keys and starts Bluetooth interface.
+ */
+ def start(): Unit = {
+ FutureHelper {
+ crypto.generateLocalKeys()
+ Log.i(Tag, "Service started, address is " + crypto.localAddress)
+ }
+ }
+
+ def stop(): Unit = {
+ transmissionInterface.destroy()
+ }
+
+ def setTransmissionInterface(interface: TransmissionInterface) = {
+ transmissionInterface = interface
+ transmissionInterface.create()
+ }
+
+ /**
+ * Sends a new message to the given target address.
+ */
+ def sendTo(target: Address, body: MessageBody): Unit = {
+ FutureHelper {
+ val messageId = settings.get("message_id", 0L)
+ val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(),
+ body.contentType, Some(messageId), Some(new Date()))
+ settings.put("message_id", messageId + 1)
+
+ val msg = new Message(header, body)
+ val encrypted = crypto.encrypt(crypto.sign(msg))
+ router.onReceive(encrypted)
+ onNewMessage(msg)
+ }
+ }
+
+ private def sendVia(nextHop: Address, msg: Message) =
+ transmissionInterface.send(nextHop, msg)
+
+ /**
+ * Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]].
+ */
+ def onMessageReceived(msg: Message): Unit = {
+ if (msg.header.target == crypto.localAddress) {
+ val decrypted = crypto.decrypt(msg)
+ if (!crypto.verify(decrypted)) {
+ Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
+ return
+ }
+ onNewMessage(decrypted)
+ } else {
+ router.onReceive(msg)
+ }
+ }
+
+ /**
+ * Handles all (locally and remotely sent) new messages.
+ */
+ private def onNewMessage(msg: Message): Unit = msg.body match {
+ case ui: UserInfo =>
+ val contact = new User(msg.header.origin, ui.name, ui.status)
+ knownUsers += contact
+ if (database.getContact(msg.header.origin).nonEmpty)
+ database.updateContact(contact)
+
+ callbacks.onConnectionsChanged()
+ case _ =>
+ val origin = msg.header.origin
+ if (origin != crypto.localAddress && database.getContact(origin).isEmpty)
+ database.addContact(getUser(origin))
+
+ database.onMessageReceived(msg)
+ callbacks.onMessageReceived(msg)
+ }
+
+ /**
+ * Opens connection to a direct neighbor.
+ *
+ * This adds the other node's public key if we don't have it. If we do, it validates the signature
+ * with the stored key.
+ *
+ * @param msg The message containing [[ConnectionInfo]] to open the connection.
+ * @return True if the connection is valid
+ */
+ def onConnectionOpened(msg: Message): Boolean = {
+ val maxConnections = settings.get(Settings.KeyMaxConnections, Settings.DefaultMaxConnections.toString).toInt
+ if (connections().size == maxConnections) {
+ Log.i(Tag, "Maximum number of connections reached")
+ return false
+ }
+
+ 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)
+ return false
+ }
+
+ if (crypto.havePublicKey(sender) && !crypto.verify(msg, crypto.getPublicKey(sender))) {
+ Log.i(Tag, "Ignoring ConnectionInfo message with invalid signature")
+ return false
+ }
+
+ synchronized {
+ if (!crypto.havePublicKey(sender)) {
+ crypto.addPublicKey(sender, info.key)
+ Log.i(Tag, "Added public key for new device " + sender.toString)
+ }
+ }
+
+ Log.i(Tag, "Node " + sender + " connected")
+ sendTo(sender, new UserInfo(settings.get(Settings.KeyUserName, ""),
+ settings.get(Settings.KeyUserStatus, "")))
+ callbacks.onConnectionsChanged()
+ true
+ }
+
+ def onConnectionClosed() = callbacks.onConnectionsChanged()
+
+ def connections() = transmissionInterface.getConnections
+
+ def getUser(address: Address) =
+ knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, ""))
+
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala b/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala
similarity index 89%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala
index 747da08..655ff58 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala
@@ -1,4 +1,4 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
import java.io._
import java.security._
@@ -6,13 +6,10 @@ import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import javax.crypto.spec.SecretKeySpec
import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
-import android.content.Context
-import android.preference.PreferenceManager
-import android.util.Log
-import com.nutomic.ensichat.protocol.Crypto._
-import com.nutomic.ensichat.protocol.body._
-import com.nutomic.ensichat.protocol.header.ContentHeader
-import com.nutomic.ensichat.util.PRNGFixes
+import com.nutomic.ensichat.core.Crypto._
+import com.nutomic.ensichat.core.body._
+import com.nutomic.ensichat.core.header.ContentHeader
+import com.nutomic.ensichat.core.interfaces.{Log, Settings}
object Crypto {
@@ -39,7 +36,12 @@ object Crypto {
/**
* Algorithm used for symmetric message encryption.
*/
- val SymmetricKeyAlgorithm = "AES/CBC/PKCS5Padding"
+ val SymmetricKeyAlgorithm = "AES"
+
+ /**
+ * Length of the symmetric message encryption key in bits.
+ */
+ val SymmetricKeyLength = 128
/**
* Algorithm used to hash PublicKey and get the address.
@@ -57,15 +59,12 @@ object Crypto {
/**
* Handles all cryptography related operations.
*
- * @note We can't use [[KeyStore]], because it requires certificates, and does not work for
- * private keys
+ * @param keyFolder Folder where private and public keys are stored.
*/
-class Crypto(context: Context) {
+class Crypto(settings: Settings, keyFolder: File) {
private val Tag = "Crypto"
- PRNGFixes.apply()
-
/**
* Generates a new key pair using [[KeyAlgorithm]] with [[KeySize]] bits and stores the keys.
*
@@ -87,10 +86,7 @@ class Crypto(context: Context) {
// The hash must have at least one bit set to not collide with the broadcast address.
} while(address == Address.Broadcast || address == Address.Null)
- PreferenceManager.getDefaultSharedPreferences(context)
- .edit()
- .putString(Crypto.LocalAddressKey, address.toString)
- .commit()
+ settings.put(LocalAddressKey, address.toString)
saveKey(PrivateKeyAlias, keyPair.getPrivate)
saveKey(PublicKeyAlias, keyPair.getPublic)
@@ -169,7 +165,7 @@ class Crypto(context: Context) {
", aborting")
}
- keyFolder.mkdir()
+ keyFolder.mkdirs()
var fos: Option[FileOutputStream] = None
try {
fos = Option(new FileOutputStream(path))
@@ -218,11 +214,6 @@ class Crypto(context: Context) {
}
}
- /**
- * Returns the folder where keys are stored.
- */
- private def keyFolder = new File(context.getFilesDir, "keys")
-
def encrypt(msg: Message, key: PublicKey = null): Message = {
assert(msg.crypto.signature.isDefined, "Message must be signed before encryption")
@@ -287,7 +278,7 @@ class Crypto(context: Context) {
*/
private def makeSecretKey(): SecretKey = {
val kgen = KeyGenerator.getInstance(SymmetricCipherAlgorithm)
- kgen.init(256)
+ kgen.init(SymmetricKeyLength)
val key = kgen.generateKey()
new SecretKeySpec(key.getEncoded, SymmetricKeyAlgorithm)
}
@@ -304,7 +295,6 @@ class Crypto(context: Context) {
/**
* Returns the address of the local node.
*/
- def localAddress = new Address(
- PreferenceManager.getDefaultSharedPreferences(context).getString(LocalAddressKey, null))
+ def localAddress = new Address(settings.get(LocalAddressKey, ""))
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Message.scala b/core/src/main/scala/com/nutomic/ensichat/core/Message.scala
similarity index 91%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/Message.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/Message.scala
index 23b933a..93d436b 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/Message.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Message.scala
@@ -1,10 +1,10 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
import java.io.InputStream
import java.security.spec.InvalidKeySpecException
-import com.nutomic.ensichat.protocol.body._
-import com.nutomic.ensichat.protocol.header.{AbstractHeader, ContentHeader, MessageHeader}
+import com.nutomic.ensichat.core.body.{ConnectionInfo, CryptoData, EncryptedBody, MessageBody}
+import com.nutomic.ensichat.core.header.{AbstractHeader, ContentHeader, MessageHeader}
object Message {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala b/core/src/main/scala/com/nutomic/ensichat/core/Router.scala
similarity index 95%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/Router.scala
index 27716ae..d138922 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/Router.scala
@@ -1,6 +1,6 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
-import com.nutomic.ensichat.protocol.header.{MessageHeader, ContentHeader}
+import com.nutomic.ensichat.core.header.{ContentHeader, MessageHeader}
/**
* Forwards messages to all connected devices.
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala b/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala
new file mode 100644
index 0000000..3674602
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala
@@ -0,0 +1,21 @@
+package com.nutomic.ensichat.core
+
+import com.nutomic.ensichat.core.header.ContentHeader
+import com.nutomic.ensichat.core.interfaces.Settings
+
+/**
+ * Generates sequence numbers according to protocol, which are stored persistently.
+ */
+class SeqNumGenerator(preferences: Settings) {
+
+ private val KeySequenceNumber = "sequence_number"
+
+ private var current = preferences.get(KeySequenceNumber, ContentHeader.SeqNumRange.head)
+
+ def next(): Int = {
+ current += 1
+ preferences.put(KeySequenceNumber, current)
+ current
+ }
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/User.scala b/core/src/main/scala/com/nutomic/ensichat/core/User.scala
new file mode 100644
index 0000000..e82e073
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/User.scala
@@ -0,0 +1,3 @@
+package com.nutomic.ensichat.core
+
+case class User(address: Address, name: String, status: String)
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/body/ConnectionInfo.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala
similarity index 88%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/body/ConnectionInfo.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala
index 06a5431..c17a65f 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/body/ConnectionInfo.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala
@@ -1,11 +1,11 @@
-package com.nutomic.ensichat.protocol.body
+package com.nutomic.ensichat.core.body
import java.nio.ByteBuffer
import java.security.spec.X509EncodedKeySpec
import java.security.{KeyFactory, PublicKey}
-import com.nutomic.ensichat.protocol.Crypto
-import com.nutomic.ensichat.util.BufferUtils
+import com.nutomic.ensichat.core.Crypto
+import com.nutomic.ensichat.core.util.BufferUtils
object ConnectionInfo {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/body/CryptoData.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala
similarity index 94%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/body/CryptoData.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala
index d703a06..f0fdbe1 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/body/CryptoData.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala
@@ -1,9 +1,9 @@
-package com.nutomic.ensichat.protocol.body
+package com.nutomic.ensichat.core.body
import java.nio.ByteBuffer
import java.util
-import com.nutomic.ensichat.util.BufferUtils
+import com.nutomic.ensichat.core.util.BufferUtils
object CryptoData {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/body/EncryptedBody.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala
similarity index 85%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/body/EncryptedBody.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala
index cd92f82..2447cbb 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/body/EncryptedBody.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala
@@ -1,4 +1,4 @@
-package com.nutomic.ensichat.protocol.body
+package com.nutomic.ensichat.core.body
/**
* Represents the data in an encrypted message body.
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/body/MessageBody.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/MessageBody.scala
similarity index 84%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/body/MessageBody.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/body/MessageBody.scala
index 8c48b96..461bd5b 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/body/MessageBody.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/MessageBody.scala
@@ -1,4 +1,4 @@
-package com.nutomic.ensichat.protocol.body
+package com.nutomic.ensichat.core.body
/**
* Holds the actual message content.
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/body/Text.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala
similarity index 87%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/body/Text.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala
index d3fb716..eda34d1 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/body/Text.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala
@@ -1,9 +1,9 @@
-package com.nutomic.ensichat.protocol.body
+package com.nutomic.ensichat.core.body
import java.nio.ByteBuffer
-import com.nutomic.ensichat.protocol.Message
-import com.nutomic.ensichat.util.BufferUtils
+import com.nutomic.ensichat.core.Message
+import com.nutomic.ensichat.core.util.BufferUtils
object Text {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/body/UserInfo.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala
similarity index 89%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/body/UserInfo.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala
index d12a4fe..d8c9cbc 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/body/UserInfo.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala
@@ -1,8 +1,9 @@
-package com.nutomic.ensichat.protocol.body
+package com.nutomic.ensichat.core.body
import java.nio.ByteBuffer
-import com.nutomic.ensichat.protocol.Message
-import com.nutomic.ensichat.util.BufferUtils
+
+import com.nutomic.ensichat.core.Message
+import com.nutomic.ensichat.core.util.BufferUtils
object UserInfo {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/header/AbstractHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/AbstractHeader.scala
similarity index 92%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/header/AbstractHeader.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/header/AbstractHeader.scala
index 9628458..a2a4589 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/header/AbstractHeader.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/header/AbstractHeader.scala
@@ -1,10 +1,10 @@
-package com.nutomic.ensichat.protocol.header
+package com.nutomic.ensichat.core.header
import java.nio.ByteBuffer
import java.util.Date
-import com.nutomic.ensichat.protocol.Address
-import com.nutomic.ensichat.util.BufferUtils
+import com.nutomic.ensichat.core.Address
+import com.nutomic.ensichat.core.util.BufferUtils
object AbstractHeader {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/header/ContentHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala
similarity index 94%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/header/ContentHeader.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala
index 538549b..1eed421 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/header/ContentHeader.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala
@@ -1,10 +1,10 @@
-package com.nutomic.ensichat.protocol.header
+package com.nutomic.ensichat.core.header
import java.nio.ByteBuffer
import java.util.Date
-import com.nutomic.ensichat.protocol.Address
-import com.nutomic.ensichat.util.BufferUtils
+import com.nutomic.ensichat.core.Address
+import com.nutomic.ensichat.core.util.BufferUtils
object ContentHeader {
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/header/MessageHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala
similarity index 88%
rename from app/src/main/scala/com/nutomic/ensichat/protocol/header/MessageHeader.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala
index 2074957..90c7b78 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/header/MessageHeader.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala
@@ -1,10 +1,10 @@
-package com.nutomic.ensichat.protocol.header
+package com.nutomic.ensichat.core.header
import java.nio.ByteBuffer
-import com.nutomic.ensichat.protocol.{Message, Address}
-import Message.ParseMessageException
-import com.nutomic.ensichat.util.BufferUtils
+import com.nutomic.ensichat.core.Address
+import com.nutomic.ensichat.core.Message.ParseMessageException
+import com.nutomic.ensichat.core.util.BufferUtils
object MessageHeader {
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala
new file mode 100644
index 0000000..e828441
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala
@@ -0,0 +1,10 @@
+package com.nutomic.ensichat.core.interfaces
+
+import com.nutomic.ensichat.core.Message
+
+trait CallbackInterface {
+
+ def onMessageReceived(msg: Message): Unit
+
+ def onConnectionsChanged(): Unit
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/DatabaseInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/DatabaseInterface.scala
new file mode 100644
index 0000000..693113e
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/DatabaseInterface.scala
@@ -0,0 +1,29 @@
+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
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala
new file mode 100644
index 0000000..c565b43
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala
@@ -0,0 +1,29 @@
+package com.nutomic.ensichat.core.interfaces
+
+object Log {
+
+ def setLogClass[T](logClass: Class[T]) = {
+ this.logClass = Option(logClass)
+ }
+
+ private var logClass: Option[Class[_]] = None
+
+ def v(tag: String, message: String, tr: Throwable = null) = log("v", tag, message, tr)
+
+ def d(tag: String, message: String, tr: Throwable = null) = log("d", tag, message, tr)
+
+ def i(tag: String, message: String, tr: Throwable = null) = log("i", tag, message, tr)
+
+ def w(tag: String, message: String, tr: Throwable = null) = log("w", tag, message, tr)
+
+ def e(tag: String, message: String, tr: Throwable = null) = log("e", tag, message, tr)
+
+ private def log(level: String, tag: String, message: String, throwable: Throwable) = logClass match {
+ case Some(l) =>
+ l.getMethod(level, classOf[String], classOf[String], classOf[Throwable])
+ .invoke(null, tag, message, throwable)
+ case None =>
+ System.out.println(level + tag + message + throwable)
+ }
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala
new file mode 100644
index 0000000..93c0824
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala
@@ -0,0 +1,36 @@
+package com.nutomic.ensichat.core.interfaces
+
+object Settings {
+
+ val KeyUserName = "user_name"
+ val KeyUserStatus = "user_status"
+ val KeyNotificationSoundsOn = "notification_sounds"
+
+ /**
+ * NOTE: Stored as string in settings.
+ */
+ val KeyScanInterval = "scan_interval_seconds"
+
+ /**
+ * NOTE: Stored as string in settings.
+ */
+ val KeyMaxConnections = "max_connections"
+
+ val DefaultUserStatus = "Let's chat!"
+ val DefaultScanInterval = 15
+ val DefaultNotificationSoundsOn = true
+ val DefaultMaxConnections = 1000000
+
+}
+
+/**
+ * Interface for persistent storage of key value pairs.
+ *
+ * Must support at least storage of strings and integers.
+ */
+trait Settings {
+
+ def put[T](key: String, value: T): Unit
+ def get[T](key: String, default: T): T
+
+}
diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/TransmissionInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/TransmissionInterface.scala
new file mode 100644
index 0000000..8831706
--- /dev/null
+++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/TransmissionInterface.scala
@@ -0,0 +1,18 @@
+package com.nutomic.ensichat.core.interfaces
+
+import com.nutomic.ensichat.core.{Address, Message}
+
+/**
+ * Transfers data to another node over a certain medium (eg Internet or Bluetooth).
+ */
+trait TransmissionInterface {
+
+ def create(): Unit
+
+ def destroy(): Unit
+
+ def send(nextHop: Address, msg: Message): Unit
+
+ def getConnections: Set[Address]
+
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala b/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala
similarity index 95%
rename from app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala
index 3b2493b..55ae730 100644
--- a/app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala
@@ -1,4 +1,4 @@
-package com.nutomic.ensichat.util
+package com.nutomic.ensichat.core.util
import java.nio.ByteBuffer
diff --git a/app/src/main/scala/com/nutomic/ensichat/util/FutureHelper.scala b/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala
similarity index 64%
rename from app/src/main/scala/com/nutomic/ensichat/util/FutureHelper.scala
rename to core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala
index 6b50d45..af9b4a9 100644
--- a/app/src/main/scala/com/nutomic/ensichat/util/FutureHelper.scala
+++ b/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala
@@ -1,6 +1,4 @@
-package com.nutomic.ensichat.util
-
-import android.os.{Looper, Handler}
+package com.nutomic.ensichat.core.util
import scala.concurrent.{ExecutionContext, Future}
@@ -12,13 +10,10 @@ import scala.concurrent.{ExecutionContext, Future}
object FutureHelper {
def apply[A](action: => A)(implicit executor: ExecutionContext): Future[A] = {
- val handler = new Handler(Looper.getMainLooper)
val f = Future(action)
f.onFailure {
case e =>
- handler.post(new Runnable {
- override def run(): Unit = throw e
- })
+ throw e
}
f
}
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/AddressTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/AddressTest.scala
similarity index 85%
rename from app/src/androidTest/scala/com/nutomic/ensichat/protocol/AddressTest.scala
rename to core/src/test/scala/com/nutomic/ensichat/core/AddressTest.scala
index 92de1ff..875fb48 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/AddressTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/AddressTest.scala
@@ -1,8 +1,8 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
-import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.AddressTest._
-import junit.framework.Assert._
+import com.nutomic.ensichat.core.AddressTest._
+import junit.framework.TestCase
+import org.junit.Assert._
object AddressTest {
@@ -24,7 +24,7 @@ object AddressTest {
}
-class AddressTest extends AndroidTestCase {
+class AddressTest extends TestCase {
def testEncode(): Unit = {
Addresses.foreach{a =>
diff --git a/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala
new file mode 100644
index 0000000..42b00b2
--- /dev/null
+++ b/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala
@@ -0,0 +1,50 @@
+package com.nutomic.ensichat.core
+
+import java.io.File
+
+import com.nutomic.ensichat.core.interfaces.Settings
+import junit.framework.TestCase
+import org.junit.Assert._
+
+object CryptoTest {
+
+ class TestSettings extends Settings {
+ private var map = Map[String, Any]()
+ override def get[T](key: String, default: T): T = map.getOrElse(key, default).asInstanceOf[T]
+ override def put[T](key: String, value: T): Unit = map += (key -> value)
+ }
+
+ def getCrypto: Crypto = {
+ val tempFolder = new File(System.getProperty("testDir"), "/crypto/")
+ val crypto = new Crypto(new TestSettings(), tempFolder)
+ if (!crypto.localKeysExist) {
+ crypto.generateLocalKeys()
+ }
+ crypto
+ }
+
+}
+
+class CryptoTest extends TestCase {
+
+ private lazy val crypto = CryptoTest.getCrypto
+
+ def testSignVerify(): Unit = {
+ MessageTest.messages.foreach { m =>
+ val signed = crypto.sign(m)
+ assertTrue(crypto.verify(signed, crypto.getLocalPublicKey))
+ assertEquals(m.header, signed.header)
+ assertEquals(m.body, signed.body)
+ }
+ }
+
+ def testEncryptDecrypt(): Unit = {
+ MessageTest.messages.foreach{ m =>
+ val encrypted = crypto.encrypt(crypto.sign(m), crypto.getLocalPublicKey)
+ val decrypted = crypto.decrypt(encrypted)
+ assertEquals(m.body, decrypted.body)
+ assertEquals(m.header, encrypted.header)
+ }
+ }
+
+}
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/MessageTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/MessageTest.scala
similarity index 68%
rename from app/src/androidTest/scala/com/nutomic/ensichat/protocol/MessageTest.scala
rename to core/src/test/scala/com/nutomic/ensichat/core/MessageTest.scala
index b94e96c..cb63f82 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/MessageTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/MessageTest.scala
@@ -1,13 +1,13 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
import java.io.ByteArrayInputStream
-import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.MessageTest._
-import com.nutomic.ensichat.protocol.body.{ConnectionInfo, ConnectionInfoTest, Text}
-import com.nutomic.ensichat.protocol.header.ContentHeaderTest._
-import com.nutomic.ensichat.protocol.header.MessageHeader
-import junit.framework.Assert._
+import com.nutomic.ensichat.core.MessageTest._
+import com.nutomic.ensichat.core.body.{ConnectionInfo, ConnectionInfoTest, Text}
+import com.nutomic.ensichat.core.header.ContentHeaderTest._
+import com.nutomic.ensichat.core.header.MessageHeader
+import junit.framework.TestCase
+import org.junit.Assert._
import scala.collection.immutable.TreeSet
@@ -23,16 +23,9 @@ object MessageTest {
}
-class MessageTest extends AndroidTestCase {
+class MessageTest extends TestCase {
- private lazy val crypto: Crypto = new Crypto(getContext)
-
- override def setUp(): Unit = {
- super.setUp()
- if (!crypto.localKeysExist) {
- crypto.generateLocalKeys()
- }
- }
+ private lazy val crypto = CryptoTest.getCrypto
def testOrder(): Unit = {
var messages = new TreeSet[Message]()(Message.Ordering)
@@ -48,7 +41,7 @@ class MessageTest extends AndroidTestCase {
def testSerializeSigned(): Unit = {
val header = new MessageHeader(ConnectionInfo.Type, AddressTest.a4, AddressTest.a2, 0)
- val m = new Message(header, ConnectionInfoTest.generateCi(getContext))
+ val m = new Message(header, ConnectionInfoTest.generateCi())
val signed = crypto.sign(m)
val bytes = signed.write
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/RouterTest.scala
similarity index 92%
rename from app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala
rename to core/src/test/scala/com/nutomic/ensichat/core/RouterTest.scala
index 298a778..f540fbb 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/RouterTest.scala
@@ -1,13 +1,13 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
import java.util.{Date, GregorianCalendar}
-import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.body.{Text, UserInfo}
-import com.nutomic.ensichat.protocol.header.ContentHeader
-import junit.framework.Assert._
+import com.nutomic.ensichat.core.body.{Text, UserInfo}
+import com.nutomic.ensichat.core.header.ContentHeader
+import junit.framework.TestCase
+import org.junit.Assert._
-class RouterTest extends AndroidTestCase {
+class RouterTest extends TestCase {
private def neighbors() = Set[Address](AddressTest.a1, AddressTest.a2, AddressTest.a3)
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/UserTest.scala
similarity index 80%
rename from app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala
rename to core/src/test/scala/com/nutomic/ensichat/core/UserTest.scala
index d9d078d..9e00134 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/UserTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/UserTest.scala
@@ -1,4 +1,4 @@
-package com.nutomic.ensichat.protocol
+package com.nutomic.ensichat.core
object UserTest {
@@ -7,5 +7,5 @@ object UserTest {
val u2 = new User(AddressTest.a2, "two", "s2")
val u3 = new User(AddressTest.a3, "three", "s3")
-
-}
\ No newline at end of file
+
+}
diff --git a/core/src/test/scala/com/nutomic/ensichat/core/body/ConnectionInfoTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/body/ConnectionInfoTest.scala
new file mode 100644
index 0000000..07bb575
--- /dev/null
+++ b/core/src/test/scala/com/nutomic/ensichat/core/body/ConnectionInfoTest.scala
@@ -0,0 +1,27 @@
+package com.nutomic.ensichat.core.body
+
+import com.nutomic.ensichat.core.CryptoTest
+import junit.framework.TestCase
+import org.junit.Assert._
+
+object ConnectionInfoTest {
+
+ def generateCi() = {
+ val crypto = CryptoTest.getCrypto
+ if (!crypto.localKeysExist)
+ crypto.generateLocalKeys()
+ new ConnectionInfo(crypto.getLocalPublicKey)
+ }
+
+}
+
+class ConnectionInfoTest extends TestCase {
+
+ def testWriteRead(): Unit = {
+ val ci = ConnectionInfoTest.generateCi()
+ val bytes = ci.write
+ val body = ConnectionInfo.read(bytes)
+ assertEquals(ci.key, body.asInstanceOf[ConnectionInfo].key)
+ }
+
+}
diff --git a/core/src/test/scala/com/nutomic/ensichat/core/body/UserInfoTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/body/UserInfoTest.scala
new file mode 100644
index 0000000..e40dc21
--- /dev/null
+++ b/core/src/test/scala/com/nutomic/ensichat/core/body/UserInfoTest.scala
@@ -0,0 +1,15 @@
+package com.nutomic.ensichat.core.body
+
+import junit.framework.TestCase
+import org.junit.Assert._
+
+class UserInfoTest extends TestCase {
+
+ def testWriteRead(): Unit = {
+ val name = new UserInfo("name", "status")
+ val bytes = name.write
+ val body = UserInfo.read(bytes)
+ assertEquals(name, body.asInstanceOf[UserInfo])
+ }
+
+}
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/header/ContentHeaderTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/header/ContentHeaderTest.scala
similarity index 78%
rename from app/src/androidTest/scala/com/nutomic/ensichat/protocol/header/ContentHeaderTest.scala
rename to core/src/test/scala/com/nutomic/ensichat/core/header/ContentHeaderTest.scala
index e416ab8..bb94cd3 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/header/ContentHeaderTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/header/ContentHeaderTest.scala
@@ -1,11 +1,11 @@
-package com.nutomic.ensichat.protocol.header
+package com.nutomic.ensichat.core.header
-import java.util.{GregorianCalendar, Date}
+import java.util.{Date, GregorianCalendar}
-import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.body.Text
-import com.nutomic.ensichat.protocol.{Address, AddressTest}
-import junit.framework.Assert._
+import com.nutomic.ensichat.core.body.Text
+import com.nutomic.ensichat.core.{Address, AddressTest}
+import junit.framework.TestCase
+import org.junit.Assert._
object ContentHeaderTest {
@@ -28,7 +28,7 @@ object ContentHeaderTest {
}
-class ContentHeaderTest extends AndroidTestCase {
+class ContentHeaderTest extends TestCase {
def testSerialize(): Unit = {
ContentHeaderTest.headers.foreach{h =>
diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/header/MessageHeaderTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/header/MessageHeaderTest.scala
similarity index 69%
rename from app/src/androidTest/scala/com/nutomic/ensichat/protocol/header/MessageHeaderTest.scala
rename to core/src/test/scala/com/nutomic/ensichat/core/header/MessageHeaderTest.scala
index 40295ac..cd444b9 100644
--- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/header/MessageHeaderTest.scala
+++ b/core/src/test/scala/com/nutomic/ensichat/core/header/MessageHeaderTest.scala
@@ -1,9 +1,9 @@
-package com.nutomic.ensichat.protocol.header
+package com.nutomic.ensichat.core.header
-import android.test.AndroidTestCase
-import com.nutomic.ensichat.protocol.header.MessageHeaderTest._
-import com.nutomic.ensichat.protocol.{Address, AddressTest}
-import junit.framework.Assert._
+import com.nutomic.ensichat.core.header.MessageHeaderTest._
+import com.nutomic.ensichat.core.{Address, AddressTest}
+import junit.framework.TestCase
+import org.junit.Assert._
object MessageHeaderTest {
@@ -19,7 +19,7 @@ object MessageHeaderTest {
}
-class MessageHeaderTest extends AndroidTestCase {
+class MessageHeaderTest extends TestCase {
def testSerialize(): Unit = {
headers.foreach{h =>
diff --git a/settings.gradle b/settings.gradle
index e7b4def..00e5526 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app'
+include ':android', ':core'