Split project into seperate modules for core and android (fixes #18).

This commit is contained in:
Felix Ableitner 2015-09-08 22:45:24 +02:00
parent 8c84cb2924
commit 269fe41ebf
100 changed files with 843 additions and 719 deletions

View file

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

View file

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

View file

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

View file

@ -51,7 +51,7 @@
android:value=".activities.MainActivity" />
</activity>
<service android:name=".protocol.ChatService" />
<service android:name=".service.ChatService" />
</application>

View file

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 465 B

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 332 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 759 B

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -14,20 +14,17 @@
<CheckBoxPreference
android:title="@string/notification_sounds"
android:key="notification_sounds"
android:defaultValue="@bool/default_notification_sounds" />
android:key="notification_sounds" />
<EditTextPreference
android:title="@string/scan_interval_seconds"
android:key="scan_interval_seconds"
android:defaultValue="@string/default_scan_interval"
android:inputType="number"
android:numeric="integer" />
<EditTextPreference
android:title="@string/max_connections"
android:key="max_connections"
android:defaultValue="@string/default_max_connections"
android:inputType="number"
android:numeric="integer" />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,34 +5,25 @@ 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"
}
/**
* 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 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 _ =>
}
}

View file

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

View file

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

View file

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

View file

@ -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,7 +53,9 @@ object Database {
/**
* Stores all messages and contacts in SQL database.
*/
class Database(context: Context)
class Database(context: Context) extends DatabaseInterface {
private class Helper
extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) {
override def onCreate(db: SQLiteDatabase): Unit = {
@ -63,16 +63,28 @@ class Database(context: Context)
db.execSQL(Database.CreateMessagesTable)
}
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
if (oldVersion < 2) {
db.execSQL("ALTER TABLE contacts ADD COLUMN status TEXT")
val cv = new ContentValues()
cv.put("status", "")
db.update("contacts", cv, null, null)
}
}
}
private val helper = new Helper()
def close() = helper.close()
def getMessagesCursor(address: Address): Cursor = {
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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources translatable="false">
<string name="default_user_status">Let\'s chat!</string>
<string name="default_scan_interval">15</string>
<bool name="default_notification_sounds">true</bool>
<string name="default_max_connections">1000000</string>
</resources>

View file

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

View file

@ -1,5 +0,0 @@
package com.nutomic.ensichat.protocol
import android.os.Binder
case class ChatServiceBinder (service: ChatService) extends Binder

View file

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

View file

@ -1,3 +0,0 @@
package com.nutomic.ensichat.protocol
case class User(address: Address, name: String, status: String)

View file

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

View file

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

1
core/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

23
core/build.gradle Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
package com.nutomic.ensichat.core
case class User(address: Address, name: String, status: String)

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.nutomic.ensichat.protocol.body
package com.nutomic.ensichat.core.body
/**
* Represents the data in an encrypted message body.

View file

@ -1,4 +1,4 @@
package com.nutomic.ensichat.protocol.body
package com.nutomic.ensichat.core.body
/**
* Holds the actual message content.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.nutomic.ensichat.util
package com.nutomic.ensichat.core.util
import java.nio.ByteBuffer

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.nutomic.ensichat.protocol
package com.nutomic.ensichat.core
object UserTest {

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
include ':app'
include ':android', ':core'