Split project into seperate modules for core and android (fixes #18).
0
app/.gitignore → android/.gitignore
vendored
|
@ -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 ->
|
|
@ -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]].
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -51,7 +51,7 @@
|
|||
android:value=".activities.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<service android:name=".protocol.ChatService" />
|
||||
<service android:name=".service.ChatService" />
|
||||
|
||||
</application>
|
||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 465 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 759 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
@ -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" />
|
||||
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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()
|
|
@ -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 {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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 {
|
|
@ -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 _ =>
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
|
@ -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
|
||||
|
||||
/**
|
|
@ -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
|
||||
|
||||
/**
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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, ""))
|
||||
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
|
||||
import android.os.Binder
|
||||
|
||||
case class ChatServiceBinder (service: ChatService) extends Binder
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
|
||||
case class User(address: Address, name: String, status: String)
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
/build
|
23
core/build.gradle
Normal 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
|
||||
}
|
|
@ -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
|
|
@ -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, ""))
|
||||
|
||||
}
|
|
@ -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, ""))
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
3
core/src/main/scala/com/nutomic/ensichat/core/User.scala
Normal file
|
@ -0,0 +1,3 @@
|
|||
package com.nutomic.ensichat.core
|
||||
|
||||
case class User(address: Address, name: String, status: String)
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.nutomic.ensichat.protocol.body
|
||||
package com.nutomic.ensichat.core.body
|
||||
|
||||
/**
|
||||
* Represents the data in an encrypted message body.
|
|
@ -1,4 +1,4 @@
|
|||
package com.nutomic.ensichat.protocol.body
|
||||
package com.nutomic.ensichat.core.body
|
||||
|
||||
/**
|
||||
* Holds the actual message content.
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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]
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.nutomic.ensichat.util
|
||||
package com.nutomic.ensichat.core.util
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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 =>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
package com.nutomic.ensichat.core
|
||||
|
||||
object UserTest {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
|
@ -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 =>
|
|
@ -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 =>
|
|
@ -1 +1 @@
|
|||
include ':app'
|
||||
include ':android', ':core'
|
||||
|
|