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'