Moved remaining messages to binary encoding, removed msgpack dependency.
This seems to have added some random disconnects, but there doesn't seem to be a reason for this in the code.
This commit is contained in:
parent
de86f5f121
commit
834f3ca724
43 changed files with 984 additions and 585 deletions
112
PROTOCOL.md
112
PROTOCOL.md
|
@ -7,17 +7,17 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
||||||
document are to be interpreted as described in RFC 2119.
|
document are to be interpreted as described in RFC 2119.
|
||||||
|
|
||||||
A node is a single device implementing this protocol. Each node has
|
A _node_ is a single device implementing this protocol. Each node has
|
||||||
exactl one node address.
|
exactly one node address based on its RSA key pair.
|
||||||
|
|
||||||
A node address consists of 32 bytes and is the SHA-256 hash of the
|
A _node address_ consists of 32 bytes and is the SHA-256 hash of the
|
||||||
node's public key.
|
node's public key.
|
||||||
|
|
||||||
The broadcast address is
|
The _broadcast address_ is
|
||||||
`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`
|
`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`
|
||||||
(i.e. all bits set).
|
(i.e. all bits set).
|
||||||
|
|
||||||
The null address is
|
The _null address_ is
|
||||||
`0x0000000000000000000000000000000000000000000000000000000000000000`
|
`0x0000000000000000000000000000000000000000000000000000000000000000`
|
||||||
(i.e. no bits set).
|
(i.e. no bits set).
|
||||||
|
|
||||||
|
@ -29,6 +29,27 @@ nodes MUST NOT connect to a node with either address.
|
||||||
Messages
|
Messages
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
All messages are signed using RSASSA-PKCS1-v1_5. All messages except
|
||||||
|
ConnectionInfo are encrypted using AES/CBC/PKCS5Padding, after which
|
||||||
|
the AES key is wrapped with the recipient's public RSA key.
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Header (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Encryption Data (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Body (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
### Header
|
### Header
|
||||||
|
|
||||||
Every message starts with one 32 bit word indicating the message
|
Every message starts with one 32 bit word indicating the message
|
||||||
|
@ -54,6 +75,8 @@ header is in network byte order, i.e. big endian.
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Sequence Number | Metric | Reserved |
|
| Sequence Number | Metric | Reserved |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Body Length |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
Ver specifies the protocol version number. This is currently 0. A
|
Ver specifies the protocol version number. This is currently 0. A
|
||||||
message with unknown version number MUST be ignored. The connection
|
message with unknown version number MUST be ignored. The connection
|
||||||
|
@ -83,17 +106,50 @@ Sequence number is the sequence number of either the source or target
|
||||||
node for this message, depending on type.
|
node for this message, depending on type.
|
||||||
|
|
||||||
|
|
||||||
|
### Encryption Data
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Signature Length |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Signature (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Encryption Key Length |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/ /
|
||||||
|
\ Encryption Key (variable length) \
|
||||||
|
/ /
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
Encryption key is the symmetric key that was used to encrypt the message
|
||||||
|
body.
|
||||||
|
|
||||||
|
Signature is the cryptographic signature over the (unencrypted) message
|
||||||
|
header and message body.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ConnectionInfo (Type = 0)
|
ConnectionInfo (Type = 0)
|
||||||
---------
|
---------
|
||||||
|
|
||||||
After successfully connecting to a node via Bluetooth, public keys
|
After successfully connecting to a node via Bluetooth, public keys
|
||||||
must be exchanged. Each node MUST send this as the first message over
|
are exchanged. Each node MUST send this as the first message over
|
||||||
the connection. Hop Limit MUST be 1 for this message type (i.e. it
|
the connection. Hop Limit MUST be 1 for this message type (i.e. it
|
||||||
must never be forwarded). Origin Address and Target Address MUST be
|
must never be forwarded). Origin Address and Target Address MUST be
|
||||||
set to all zeros, and MUST be ignored by the receiving node.
|
set to all zeros, and MUST be ignored by the receiving node.
|
||||||
|
|
||||||
A receiving node SHOULD store the key in permanent storage if it
|
A receiving node SHOULD store the key in permanent storage if it
|
||||||
hasn't already stored it earlier. This key is to be used for message
|
hasn't already stored it earlier. However, a node MAY decide to
|
||||||
|
delete these stored keys in a least-recently-used order to adhere
|
||||||
|
to storage limitations. If a key has been deleted, messages to
|
||||||
|
that node can only be sent once a new ConnectionInfo message
|
||||||
|
for it has been received.
|
||||||
|
|
||||||
|
|
||||||
|
This key is to be used for message
|
||||||
encryption when communicating with the sending node.
|
encryption when communicating with the sending node.
|
||||||
|
|
||||||
0 1 2 3
|
0 1 2 3
|
||||||
|
@ -114,20 +170,46 @@ After this message has been received, communication with normal messages
|
||||||
may start.
|
may start.
|
||||||
|
|
||||||
|
|
||||||
### Data (Data Transfer, Type = 255)
|
### RequestAddContact (Type = 4)
|
||||||
|
|
||||||
|
Sent when a user wants to add another node as a contact. After this,
|
||||||
|
a ResultAddContact message should be returned.
|
||||||
|
|
||||||
0 1 2 3
|
0 1 2 3
|
||||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| Length |
|
| Reserved |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
|
||||||
|
### ResultAddContact (Type = 5)
|
||||||
|
|
||||||
|
Sent as response to a RequestAddContact message.
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|A| Reserved |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
|
Accepted bit (A) is true if the user accepts the new contact, false
|
||||||
|
otherwise. Nodes should only add another node as a contact if both
|
||||||
|
users agreed.
|
||||||
|
|
||||||
|
### Text (Type = 6)
|
||||||
|
|
||||||
|
A simple chat message.
|
||||||
|
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Reserved |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Text Length |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
/ /
|
/ /
|
||||||
\ Data (variable length) \
|
\ Text (variable length) \
|
||||||
/ /
|
/ /
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
Length is the number of bytes in data.
|
Text the string to be transferred, encoded as UTF-8.
|
||||||
|
|
||||||
Data is any binary data that should be transported.
|
|
||||||
|
|
||||||
This message type is deprecated.
|
|
||||||
|
|
|
@ -6,27 +6,19 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:0.14.4"
|
|
||||||
classpath "jp.leafytree.gradle:gradle-android-scala-plugin:1.3.1"
|
classpath "jp.leafytree.gradle:gradle-android-scala-plugin:1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile "com.android.support:support-v4:21.0.0"
|
compile "com.android.support:support-v4:21.0.2"
|
||||||
// For `flat` debug config, as `flatProvided` is unknown.
|
// For `flat` debug config, as `flatProvided` is unknown.
|
||||||
provided "org.scala-lang:scala-library:2.11.4"
|
provided "org.scala-lang:scala-library:2.11.4"
|
||||||
debugCompile "org.scala-lang:scala-library:2.11.4"
|
debugCompile "org.scala-lang:scala-library:2.11.4"
|
||||||
releaseCompile "org.scala-lang:scala-library:2.11.4"
|
releaseCompile "org.scala-lang:scala-library:2.11.4"
|
||||||
compile("org.msgpack:msgpack-scala_2.11:0.6.11") {
|
|
||||||
transitive = false;
|
|
||||||
}
|
|
||||||
compile('org.msgpack:msgpack:0.6.11') {
|
|
||||||
transitive = false;
|
|
||||||
}
|
|
||||||
compile 'com.google.guava:guava:18.0'
|
compile 'com.google.guava:guava:18.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 21
|
compileSdkVersion 21
|
||||||
buildToolsVersion "21.1.1"
|
buildToolsVersion "21.1.1"
|
||||||
|
@ -40,13 +32,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main.scala.srcDir "src/main/scala"
|
||||||
scala.srcDir "src/main/scala"
|
androidTest.scala.srcDir "src/androidTest/scala"
|
||||||
}
|
|
||||||
|
|
||||||
androidTest {
|
|
||||||
scala.srcDir "src/androidTest/scala"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -68,11 +55,13 @@ android {
|
||||||
|
|
||||||
// Needed to rename `app-thin.apk` to `app-debug.apk` (because Android Studio doesn't let us
|
// Needed to rename `app-thin.apk` to `app-debug.apk` (because Android Studio doesn't let us
|
||||||
// specify a different apk name).
|
// specify a different apk name).
|
||||||
|
/*
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
def apk = variant.outputFile;
|
def apk = variant.outputFile;
|
||||||
def newName = apk.name.replace("app-thin", "app-debug");
|
def newName = apk.name.replace("app-thin", "app-debug");
|
||||||
variant.outputFile = new File(apk.parentFile, newName);
|
variant.outputFile = new File(apk.parentFile, newName);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Avoid duplicate file errors during packaging.
|
// Avoid duplicate file errors during packaging.
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
|
@ -82,3 +71,7 @@ android {
|
||||||
exclude 'META-INF/NOTICE'
|
exclude 'META-INF/NOTICE'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(ScalaCompile) {
|
||||||
|
scalaCompileOptions.useCompileDaemon = true
|
||||||
|
}
|
||||||
|
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
|
@ -18,11 +18,12 @@
|
||||||
-dontpreverify
|
-dontpreverify
|
||||||
-dontwarn scala.**
|
-dontwarn scala.**
|
||||||
-keep class !scala*.** { *; }
|
-keep class !scala*.** { *; }
|
||||||
-ignorewarnings
|
|
||||||
# Avoid crash when invoking String.toInt (see https://issues.scala-lang.org/browse/SI-5397).
|
# Avoid crash when invoking String.toInt (see https://issues.scala-lang.org/browse/SI-5397).
|
||||||
-keep class scala.collection.SeqLike {
|
-keep class scala.collection.SeqLike {
|
||||||
public protected *;
|
public protected *;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Suppress warnings caused by msgpack (code works fine anyway).
|
# Disable warnings for Guava annotations.
|
||||||
-dontwarn
|
-dontwarn javax.annotation.**
|
||||||
|
-dontwarn javax.inject.**
|
||||||
|
-dontwarn sun.misc.Unsafe
|
||||||
|
|
|
@ -14,14 +14,17 @@ object AddressTest {
|
||||||
|
|
||||||
val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
||||||
|
|
||||||
val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104, -8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte)
|
val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104,
|
||||||
|
-8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte)
|
||||||
|
|
||||||
|
val Addresses = Set(a1, a2, a3, a4, Address.Broadcast, Address.Null)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddressTest extends AndroidTestCase {
|
class AddressTest extends AndroidTestCase {
|
||||||
|
|
||||||
def testEncode(): Unit = {
|
def testEncode(): Unit = {
|
||||||
Set(Address.Broadcast, Address.Null, a1, a2, a3, a4).foreach{a =>
|
Addresses.foreach{a =>
|
||||||
val base32 = a.toString
|
val base32 = a.toString
|
||||||
val read = new Address(base32)
|
val read = new Address(base32)
|
||||||
assertEquals(a, read)
|
assertEquals(a, read)
|
||||||
|
|
|
@ -1,30 +1,41 @@
|
||||||
package com.nutomic.ensichat.aodvv2
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
import java.util.Date
|
import java.util.GregorianCalendar
|
||||||
|
|
||||||
import android.test.AndroidTestCase
|
import android.test.AndroidTestCase
|
||||||
import junit.framework.Assert
|
import com.nutomic.ensichat.aodvv2.MessageHeaderTest._
|
||||||
|
import junit.framework.Assert._
|
||||||
|
|
||||||
object MessageHeaderTest {
|
object MessageHeaderTest {
|
||||||
|
|
||||||
val h1 = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit, new Date(), AddressTest.a3,
|
val h1 = new MessageHeader(Text.Type, MessageHeader.DefaultHopLimit, AddressTest.a1,
|
||||||
AddressTest.a4, 456, 123)
|
AddressTest.a2, 1234, 0, new GregorianCalendar(1970, 1, 1).getTime, 567, 8)
|
||||||
|
|
||||||
val h2 = new MessageHeader(0xfff, 0, new Date(0xffffffff), Address.Null, Address.Broadcast, 0,
|
val h2 = new MessageHeader(Text.Type, 0, AddressTest.a1, AddressTest.a3, 8765, 234,
|
||||||
0xff)
|
new GregorianCalendar(2014, 6, 10).getTime, 0, 0xff)
|
||||||
|
|
||||||
val h3 = new MessageHeader(0xfff, 0xff, new Date(0), Address.Broadcast, Address.Null, 0xffff, 0)
|
val h3 = new MessageHeader(Text.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56,
|
||||||
|
new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0)
|
||||||
|
|
||||||
|
val h4 = new MessageHeader(0xfff, 0, Address.Null, Address.Broadcast, 0, 0xff,
|
||||||
|
new GregorianCalendar(1990, 1, 1).getTime, 0, 0xff)
|
||||||
|
|
||||||
|
val h5 = new MessageHeader(ConnectionInfo.Type, 0xff, Address.Broadcast, Address.Null, 0xffff, 0,
|
||||||
|
new GregorianCalendar(2035, 12, 31).getTime, 0xffff, 0)
|
||||||
|
|
||||||
|
val headers = Set(h1, h2, h3, h4, h5)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageHeaderTest extends AndroidTestCase {
|
class MessageHeaderTest extends AndroidTestCase {
|
||||||
|
|
||||||
def testSerialize(): Unit = {
|
def testSerialize(): Unit = {
|
||||||
val ci = ConnectionInfoTest.generateCi(getContext)
|
headers.foreach{h =>
|
||||||
val bytes = MessageHeaderTest.h1.write(ci)
|
val bytes = h.write(0)
|
||||||
val header = MessageHeader.read(bytes)
|
val header = MessageHeader.read(bytes)
|
||||||
Assert.assertEquals(MessageHeaderTest.h1, header)
|
assertEquals(h, header)
|
||||||
Assert.assertEquals(bytes.length, header.Length)
|
assertEquals(bytes.length, header.Length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
|
||||||
|
import android.test.AndroidTestCase
|
||||||
|
import com.nutomic.ensichat.aodvv2.MessageHeaderTest._
|
||||||
|
import com.nutomic.ensichat.aodvv2.MessageTest._
|
||||||
|
import com.nutomic.ensichat.messages.Crypto
|
||||||
|
import junit.framework.Assert._
|
||||||
|
|
||||||
|
import scala.collection.immutable.TreeSet
|
||||||
|
|
||||||
|
object MessageTest {
|
||||||
|
|
||||||
|
val m1 = new Message(h1, new Text("first"))
|
||||||
|
|
||||||
|
val m2 = new Message(h2, new Text("second"))
|
||||||
|
|
||||||
|
val m3 = new Message(h3, new Text("third"))
|
||||||
|
|
||||||
|
val messages = Set(m1, m2, m3)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageTest extends AndroidTestCase {
|
||||||
|
|
||||||
|
lazy val Crypto: Crypto = new Crypto(getContext)
|
||||||
|
|
||||||
|
override def setUp(): Unit = {
|
||||||
|
super.setUp()
|
||||||
|
if (!Crypto.localKeysExist) {
|
||||||
|
Crypto.generateLocalKeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def testOrder(): Unit = {
|
||||||
|
var messages = new TreeSet[Message]()(Message.Ordering)
|
||||||
|
messages += m1
|
||||||
|
messages += m2
|
||||||
|
assertEquals(m1, messages.firstKey)
|
||||||
|
|
||||||
|
messages = new TreeSet[Message]()(Message.Ordering)
|
||||||
|
messages += m2
|
||||||
|
messages += m3
|
||||||
|
assertEquals(m2, messages.firstKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
def testSerializeSigned(): Unit = {
|
||||||
|
val header = new MessageHeader(ConnectionInfo.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56,
|
||||||
|
new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0)
|
||||||
|
val m = new Message(header, ConnectionInfoTest.generateCi(getContext))
|
||||||
|
|
||||||
|
val signed = Crypto.sign(m)
|
||||||
|
val bytes = signed.write
|
||||||
|
val read = Message.read(new ByteArrayInputStream(bytes))
|
||||||
|
|
||||||
|
assertEquals(signed, read)
|
||||||
|
assertTrue(Crypto.verify(read, Crypto.getLocalPublicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
def testSerializeEncrypted(): Unit = {
|
||||||
|
messages.foreach{ m =>
|
||||||
|
val signed = Crypto.sign(m)
|
||||||
|
val encrypted = Crypto.encrypt(signed, Crypto.getLocalPublicKey)
|
||||||
|
val bytes = encrypted.write
|
||||||
|
|
||||||
|
val read = Message.read(new ByteArrayInputStream(bytes))
|
||||||
|
assertEquals(encrypted.Crypto, read.Crypto)
|
||||||
|
val decrypted = Crypto.decrypt(read)
|
||||||
|
assertEquals(m.Header, decrypted.Header)
|
||||||
|
assertEquals(m.Body, decrypted.Body)
|
||||||
|
assertTrue(Crypto.verify(decrypted, Crypto.getLocalPublicKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import android.test.AndroidTestCase
|
||||||
|
import junit.framework.Assert._
|
||||||
|
|
||||||
|
|
||||||
|
class ResultAddContactTest extends AndroidTestCase {
|
||||||
|
|
||||||
|
def testWriteRead(): Unit = {
|
||||||
|
Array(true, false).foreach { a =>
|
||||||
|
val rac = new ResultAddContact(a)
|
||||||
|
val bytes = rac.write
|
||||||
|
val read = ResultAddContact.read(bytes)
|
||||||
|
assertEquals(a, read.Accepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.nutomic.ensichat.messages
|
package com.nutomic.ensichat.messages
|
||||||
|
|
||||||
import android.test.AndroidTestCase
|
import android.test.AndroidTestCase
|
||||||
import com.nutomic.ensichat.messages.MessageTest._
|
import com.nutomic.ensichat.aodvv2.MessageTest._
|
||||||
import junit.framework.Assert._
|
import junit.framework.Assert._
|
||||||
|
|
||||||
class CryptoTest extends AndroidTestCase {
|
class CryptoTest extends AndroidTestCase {
|
||||||
|
@ -16,15 +16,21 @@ class CryptoTest extends AndroidTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
def testSignVerify(): Unit = {
|
def testSignVerify(): Unit = {
|
||||||
val sig = Crypto.calculateSignature(m1)
|
messages.foreach { m =>
|
||||||
assertTrue(Crypto.isValidSignature(m1, sig, Crypto.getLocalPublicKey))
|
val signed = Crypto.sign(m)
|
||||||
|
assertTrue(Crypto.verify(signed, Crypto.getLocalPublicKey))
|
||||||
|
assertEquals(m.Header, signed.Header)
|
||||||
|
assertEquals(m.Body, signed.Body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def testEncryptDecrypt(): Unit = {
|
def testEncryptDecrypt(): Unit = {
|
||||||
val (encrypted, key) =
|
messages.foreach{ m =>
|
||||||
Crypto.encrypt(null, MessageTest.m1.write(Array[Byte]()), Crypto.getLocalPublicKey)
|
val encrypted = Crypto.encrypt(Crypto.sign(m), Crypto.getLocalPublicKey)
|
||||||
val decrypted = Crypto.decrypt(encrypted, key)
|
val decrypted = Crypto.decrypt(encrypted)
|
||||||
assertEquals(MessageTest.m1, Message.read(decrypted)._1)
|
assertEquals(m.Body, decrypted.Body)
|
||||||
|
assertEquals(m.Header, encrypted.Header)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package com.nutomic.ensichat.messages
|
|
||||||
|
|
||||||
import java.io.{PipedInputStream, PipedOutputStream}
|
|
||||||
import java.util.GregorianCalendar
|
|
||||||
|
|
||||||
import android.test.AndroidTestCase
|
|
||||||
import com.nutomic.ensichat.aodvv2.AddressTest
|
|
||||||
import com.nutomic.ensichat.messages.MessageTest._
|
|
||||||
import junit.framework.Assert._
|
|
||||||
|
|
||||||
import scala.collection.immutable.TreeSet
|
|
||||||
|
|
||||||
object MessageTest {
|
|
||||||
|
|
||||||
val m1 = new TextMessage(AddressTest.a1, AddressTest.a2,
|
|
||||||
new GregorianCalendar(2014, 10, 29).getTime, "first")
|
|
||||||
|
|
||||||
val m2 = new TextMessage(AddressTest.a1, AddressTest.a3,
|
|
||||||
new GregorianCalendar(2014, 10, 30).getTime, "second")
|
|
||||||
|
|
||||||
val m3 = new TextMessage(AddressTest.a4, AddressTest.a2,
|
|
||||||
new GregorianCalendar(2014, 10, 31).getTime, "third")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MessageTest extends AndroidTestCase {
|
|
||||||
|
|
||||||
def testSerialize(): Unit = {
|
|
||||||
Set(m1, m2, m3).foreach { m =>
|
|
||||||
val pis = new PipedInputStream()
|
|
||||||
val pos = new PipedOutputStream(pis)
|
|
||||||
val bytes = m.write(Array[Byte]())
|
|
||||||
val (msg, _) = Message.read(bytes)
|
|
||||||
assertEquals(m, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def testOrder(): Unit = {
|
|
||||||
var messages = new TreeSet[Message]()(Message.Ordering)
|
|
||||||
messages += MessageTest.m1
|
|
||||||
messages += MessageTest.m2
|
|
||||||
assertEquals(MessageTest.m1, messages.firstKey)
|
|
||||||
|
|
||||||
messages = new TreeSet[Message]()(Message.Ordering)
|
|
||||||
messages += MessageTest.m2
|
|
||||||
messages += MessageTest.m3
|
|
||||||
assertEquals(MessageTest.m2, messages.firstKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ import android.database.sqlite.SQLiteDatabase
|
||||||
import android.test.AndroidTestCase
|
import android.test.AndroidTestCase
|
||||||
import android.test.mock.MockContext
|
import android.test.mock.MockContext
|
||||||
import com.nutomic.ensichat.aodvv2.AddressTest
|
import com.nutomic.ensichat.aodvv2.AddressTest
|
||||||
import com.nutomic.ensichat.messages.MessageTest
|
import com.nutomic.ensichat.aodvv2.MessageTest._
|
||||||
import junit.framework.Assert._
|
import junit.framework.Assert._
|
||||||
|
|
||||||
class DatabaseTest extends AndroidTestCase {
|
class DatabaseTest extends AndroidTestCase {
|
||||||
|
@ -27,9 +27,9 @@ class DatabaseTest extends AndroidTestCase {
|
||||||
private lazy val Database = new Database(new TestContext(getContext))
|
private lazy val Database = new Database(new TestContext(getContext))
|
||||||
|
|
||||||
override def setUp(): Unit = {
|
override def setUp(): Unit = {
|
||||||
Database.addMessage(MessageTest.m1)
|
Database.addMessage(m1)
|
||||||
Database.addMessage(MessageTest.m2)
|
Database.addMessage(m2)
|
||||||
Database.addMessage(MessageTest.m3)
|
Database.addMessage(m3)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def tearDown(): Unit = {
|
override def tearDown(): Unit = {
|
||||||
|
@ -38,22 +38,22 @@ class DatabaseTest extends AndroidTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
def testMessageCount(): Unit = {
|
def testMessageCount(): Unit = {
|
||||||
val msg1 = Database.getMessages(MessageTest.m1.sender, 1)
|
val msg1 = Database.getMessages(m1.Header.Origin, 1)
|
||||||
assertEquals(1, msg1.size)
|
assertEquals(1, msg1.size)
|
||||||
|
|
||||||
val msg2 = Database.getMessages(MessageTest.m1.sender, 3)
|
val msg2 = Database.getMessages(m1.Header.Origin, 3)
|
||||||
assertEquals(2, msg2.size)
|
assertEquals(2, msg2.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
def testMessageOrder(): Unit = {
|
def testMessageOrder(): Unit = {
|
||||||
val msg = Database.getMessages(MessageTest.m1.receiver, 1)
|
val msg = Database.getMessages(m1.Header.Target, 1)
|
||||||
assertTrue(msg.contains(MessageTest.m3))
|
assertTrue(msg.contains(m3))
|
||||||
}
|
}
|
||||||
|
|
||||||
def testMessageSelect(): Unit = {
|
def testMessageSelect(): Unit = {
|
||||||
val msg = Database.getMessages(MessageTest.m1.receiver, 2)
|
val msg = Database.getMessages(m1.Header.Target, 2)
|
||||||
assertTrue(msg.contains(MessageTest.m1))
|
assertTrue(msg.contains(m1))
|
||||||
assertTrue(msg.contains(MessageTest.m3))
|
assertTrue(msg.contains(m3))
|
||||||
}
|
}
|
||||||
|
|
||||||
def testAddContact(): Unit = {
|
def testAddContact(): Unit = {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:title="@string/scan_interval_seconds"
|
android:title="@string/scan_interval_seconds"
|
||||||
android:key="scan_interval_seconds"
|
android:key="scan_interval_seconds"
|
||||||
android:defaultValue="5"
|
android:defaultValue="15"
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
android:numeric="integer" />
|
android:numeric="integer" />
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.nutomic.ensichat.activities
|
package com.nutomic.ensichat.activities
|
||||||
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.DialogInterface.OnClickListener
|
import android.content.DialogInterface.OnClickListener
|
||||||
import android.content.{Context, DialogInterface}
|
import android.content.{Context, DialogInterface}
|
||||||
|
@ -12,10 +10,10 @@ import android.view._
|
||||||
import android.widget.AdapterView.OnItemClickListener
|
import android.widget.AdapterView.OnItemClickListener
|
||||||
import android.widget._
|
import android.widget._
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
import com.nutomic.ensichat.aodvv2.{Address, Message, RequestAddContact, ResultAddContact}
|
||||||
import com.nutomic.ensichat.bluetooth.ChatService
|
import com.nutomic.ensichat.bluetooth.ChatService
|
||||||
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||||
import com.nutomic.ensichat.messages.{Crypto, Message, RequestAddContactMessage, ResultAddContactMessage}
|
import com.nutomic.ensichat.messages.Crypto
|
||||||
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
|
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
|
||||||
|
|
||||||
import scala.collection.SortedSet
|
import scala.collection.SortedSet
|
||||||
|
@ -92,7 +90,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service.send(new RequestAddContactMessage(Crypto.getLocalAddress, address, new Date()))
|
service.sendTo(address, new RequestAddContact())
|
||||||
addDeviceDialog(address)
|
addDeviceDialog(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,12 +106,10 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
|
||||||
currentlyAdding +=
|
currentlyAdding +=
|
||||||
(address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true))
|
(address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true))
|
||||||
addContactIfBothConfirmed(address)
|
addContactIfBothConfirmed(address)
|
||||||
service.send(
|
service.sendTo(address, new ResultAddContact(true))
|
||||||
new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), true))
|
|
||||||
case DialogInterface.BUTTON_NEGATIVE =>
|
case DialogInterface.BUTTON_NEGATIVE =>
|
||||||
// Local user denied adding contact, send info to other device.
|
// Local user denied adding contact, send info to other device.
|
||||||
service.send(
|
service.sendTo(address, new ResultAddContact(false))
|
||||||
new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), false))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,27 +133,28 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles incoming [[RequestAddContactMessage]] and [[ResultAddContactMessage]] messages.
|
* Handles incoming [[RequestAddContact]] and [[ResultAddContact]] messages.
|
||||||
*
|
*
|
||||||
* These are only handled here and require user action, so contacts can only be added if
|
* These are only handled here and require user action, so contacts can only be added if
|
||||||
* the user is in this activity.
|
* the user is in this activity.
|
||||||
*/
|
*/
|
||||||
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
||||||
messages.filter(_.receiver == Crypto.getLocalAddress)
|
messages.filter(_.Header.Target == Crypto.getLocalAddress)
|
||||||
.foreach{
|
.foreach{
|
||||||
case m: RequestAddContactMessage =>
|
case m if m.Body.isInstanceOf[RequestAddContact] =>
|
||||||
Log.i(Tag, "Remote device " + m.sender + " wants to add us as a contact, showing dialog")
|
Log.i(Tag, "Remote device " + m.Header.Origin + " wants to add us as a contact, showing dialog")
|
||||||
addDeviceDialog(m.sender)
|
addDeviceDialog(m.Header.Origin)
|
||||||
case m: ResultAddContactMessage =>
|
case m if m.Body.isInstanceOf[ResultAddContact] =>
|
||||||
if (m.Accepted) {
|
val origin = m.Header.Origin
|
||||||
Log.i(Tag, "Remote device " + m.sender + " accepted us as a contact, updating state")
|
if (m.Body.asInstanceOf[ResultAddContact].Accepted) {
|
||||||
currentlyAdding += (m.sender ->
|
Log.i(Tag, "Remote device " + origin + " accepted us as a contact, updating state")
|
||||||
new AddContactInfo(true, currentlyAdding(m.sender).remoteConfirmed))
|
currentlyAdding += (origin ->
|
||||||
addContactIfBothConfirmed(m.sender)
|
new AddContactInfo(true, currentlyAdding(origin).remoteConfirmed))
|
||||||
|
addContactIfBothConfirmed(origin)
|
||||||
} else {
|
} else {
|
||||||
Log.i(Tag, "Remote device " + m.sender + " denied us as a contact, showing toast")
|
Log.i(Tag, "Remote device " + origin + " denied us as a contact, showing toast")
|
||||||
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
||||||
currentlyAdding -= m.sender
|
currentlyAdding -= origin
|
||||||
}
|
}
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,11 +34,22 @@ object ConnectionInfo {
|
||||||
*/
|
*/
|
||||||
class ConnectionInfo(val key: PublicKey) extends MessageBody {
|
class ConnectionInfo(val key: PublicKey) extends MessageBody {
|
||||||
|
|
||||||
|
override def Type = ConnectionInfo.Type
|
||||||
|
|
||||||
override def write: Array[Byte] = {
|
override def write: Array[Byte] = {
|
||||||
val b = ByteBuffer.allocate(4 + key.getEncoded.length)
|
val b = ByteBuffer.allocate(length)
|
||||||
BufferUtils.putUnsignedInt(b, key.getEncoded.length)
|
BufferUtils.putUnsignedInt(b, key.getEncoded.length)
|
||||||
b.put(key.getEncoded)
|
b.put(key.getEncoded)
|
||||||
b.array()
|
b.array()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def equals(a: Any): Boolean = a match {
|
||||||
|
case o: ConnectionInfo => key == o.key
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
override def toString = "ConnectionInfo(key=" + key + ")"
|
||||||
|
|
||||||
|
override def length = 4 + key.getEncoded.length
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
import com.nutomic.ensichat.util.BufferUtils
|
||||||
|
|
||||||
|
object CryptoData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs [[CryptoData]] instance from byte array.
|
||||||
|
*/
|
||||||
|
def read(array: Array[Byte]): (CryptoData, Array[Byte]) = {
|
||||||
|
val b = ByteBuffer.wrap(array)
|
||||||
|
val signatureLength = BufferUtils.getUnsignedInt(b).toInt
|
||||||
|
val signature = new Array[Byte](signatureLength)
|
||||||
|
b.get(signature, 0, signatureLength)
|
||||||
|
|
||||||
|
val keyLength = BufferUtils.getUnsignedInt(b).toInt
|
||||||
|
val key =
|
||||||
|
if (keyLength != 0) {
|
||||||
|
val key = new Array[Byte](keyLength)
|
||||||
|
b.get(key, 0, keyLength)
|
||||||
|
Some(key)
|
||||||
|
}
|
||||||
|
else None
|
||||||
|
|
||||||
|
val remaining = new Array[Byte](b.remaining())
|
||||||
|
b.get(remaining, 0, b.remaining())
|
||||||
|
(new CryptoData(Some(signature), key), remaining)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the signature and (optional) key that are stored in a message.
|
||||||
|
*/
|
||||||
|
class CryptoData(val Signature: Option[Array[Byte]], val Key: Option[Array[Byte]]) {
|
||||||
|
|
||||||
|
override def equals(a: Any): Boolean = a match {
|
||||||
|
case o: CryptoData =>
|
||||||
|
Arrays.equals(Signature.orNull, o.Signature.orNull) && Arrays.equals(Key.orNull, o.Key.orNull)
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes this object into a new byte array.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
def write: Array[Byte] = {
|
||||||
|
val b = ByteBuffer.allocate(length)
|
||||||
|
BufferUtils.putUnsignedInt(b, Signature.get.length)
|
||||||
|
b.put(Signature.get)
|
||||||
|
BufferUtils.putUnsignedInt(b, keyLength)
|
||||||
|
if (Key.nonEmpty) b.put(Key.get)
|
||||||
|
b.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
def length = 8 + Signature.get.length + keyLength
|
||||||
|
|
||||||
|
private def keyLength = if (Key.isDefined) Key.get.length else 0
|
||||||
|
|
||||||
|
override def toString = "CryptoData(Signature.length=" + Signature.foreach(_.length) +
|
||||||
|
", Key.length=" + Key.foreach(_.length) + ")"
|
||||||
|
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
package com.nutomic.ensichat.aodvv2
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
import com.nutomic.ensichat.util.BufferUtils
|
|
||||||
|
|
||||||
object Data {
|
|
||||||
|
|
||||||
val Type = 255
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs [[Data]] object from byte array.
|
|
||||||
*/
|
|
||||||
def read(array: Array[Byte]): Data = {
|
|
||||||
val b = ByteBuffer.wrap(array)
|
|
||||||
val length = BufferUtils.getUnsignedInt(b).toInt
|
|
||||||
val data = new Array[Byte](length)
|
|
||||||
b.get(data, 0, length)
|
|
||||||
new Data(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Container for [[com.nutomic.ensichat.messages.Message]] objects.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
class Data(val data: Array[Byte]) extends MessageBody {
|
|
||||||
|
|
||||||
override def write: Array[Byte] = {
|
|
||||||
val b = ByteBuffer.allocate(4 + data.length)
|
|
||||||
BufferUtils.putUnsignedInt(b, data.length)
|
|
||||||
b.put(data)
|
|
||||||
b.array()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the data in an encrypted message body.
|
||||||
|
*/
|
||||||
|
class EncryptedBody(val Data: Array[Byte]) extends MessageBody {
|
||||||
|
|
||||||
|
override def Type = -1
|
||||||
|
|
||||||
|
def write = Data
|
||||||
|
|
||||||
|
override def toString = "EncryptedBody(Data.length=" + Data.length + ")"
|
||||||
|
|
||||||
|
override def length = Data.length
|
||||||
|
}
|
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala
Normal file
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object Message {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders messages by date, oldest messages first.
|
||||||
|
*/
|
||||||
|
val Ordering = new Ordering[Message] {
|
||||||
|
override def compare(m1: Message, m2: Message) = m1.Header.Time.compareTo(m2.Header.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
def read(stream: InputStream): Message = {
|
||||||
|
val headerBytes = new Array[Byte](MessageHeader.Length)
|
||||||
|
stream.read(headerBytes, 0, MessageHeader.Length)
|
||||||
|
val header = MessageHeader.read(headerBytes)
|
||||||
|
|
||||||
|
val contentLength = (header.Length - MessageHeader.Length).toInt
|
||||||
|
val contentBytes = new Array[Byte](contentLength)
|
||||||
|
stream.read(contentBytes, 0, contentLength)
|
||||||
|
|
||||||
|
val (crypto, remaining) = CryptoData.read(contentBytes)
|
||||||
|
|
||||||
|
val body =
|
||||||
|
header.MessageType match {
|
||||||
|
case ConnectionInfo.Type => ConnectionInfo.read(remaining)
|
||||||
|
case _ => new EncryptedBody(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
new Message(header, crypto, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Message(val Header: MessageHeader, val Crypto: CryptoData, val Body: MessageBody) {
|
||||||
|
|
||||||
|
def this(header: MessageHeader, body: MessageBody) =
|
||||||
|
this(header, new CryptoData(None, None), body)
|
||||||
|
|
||||||
|
def write = Header.write(Body.length + Crypto.length) ++ Crypto.write ++ Body.write
|
||||||
|
|
||||||
|
override def toString = "Message(Header=" + Header + ", Body=" + Body + ", Crypto=" + Crypto + ")"
|
||||||
|
|
||||||
|
override def equals(a: Any): Boolean = a match {
|
||||||
|
case o: Message => Header == o.Header && Body == o.Body && Crypto == o.Crypto
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +1,19 @@
|
||||||
package com.nutomic.ensichat.aodvv2
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the actual message content.
|
* Holds the actual message content.
|
||||||
*/
|
*/
|
||||||
abstract class MessageBody {
|
abstract class MessageBody {
|
||||||
|
|
||||||
|
def Type: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the message contents to a byte array.
|
* Writes the message contents to a byte array.
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
def write: Array[Byte]
|
def write: Array[Byte]
|
||||||
|
|
||||||
|
def length: Int
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,11 @@ import com.nutomic.ensichat.util.BufferUtils
|
||||||
|
|
||||||
object MessageHeader {
|
object MessageHeader {
|
||||||
|
|
||||||
val Length = 16 + 2 * Address.Length
|
val Length = 20 + 2 * Address.Length
|
||||||
|
|
||||||
val DefaultHopLimit = 20
|
val DefaultHopLimit = 20
|
||||||
|
|
||||||
val Version = 3
|
val Version = 0
|
||||||
|
|
||||||
class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) {
|
class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) {
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ object MessageHeader {
|
||||||
val seqNum = BufferUtils.getUnsignedShort(b)
|
val seqNum = BufferUtils.getUnsignedShort(b)
|
||||||
val metric = BufferUtils.getUnsignedByte(b)
|
val metric = BufferUtils.getUnsignedByte(b)
|
||||||
|
|
||||||
new MessageHeader(messageType, hopLimit, time, origin, target, seqNum, metric, length, hopCount)
|
new MessageHeader(messageType, hopLimit, origin, target, seqNum, metric, time, length, hopCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -48,27 +48,26 @@ object MessageHeader {
|
||||||
*/
|
*/
|
||||||
class MessageHeader(val MessageType: Int,
|
class MessageHeader(val MessageType: Int,
|
||||||
val HopLimit: Int,
|
val HopLimit: Int,
|
||||||
val Time: Date,
|
|
||||||
val Origin: Address,
|
val Origin: Address,
|
||||||
val Target: Address,
|
val Target: Address,
|
||||||
val SequenceNumber: Int,
|
val SequenceNumber: Int,
|
||||||
val Metric: Int,
|
val Metric: Int,
|
||||||
|
val Time: Date = new Date(),
|
||||||
val Length: Long = -1,
|
val Length: Long = -1,
|
||||||
val HopCount: Int = 0) {
|
val HopCount: Int = 0) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the header to byte array.
|
* Writes the header to byte array.
|
||||||
*/
|
*/
|
||||||
def write(body: MessageBody): Array[Byte] = {
|
def write(contentLength: Int): Array[Byte] = {
|
||||||
val b = ByteBuffer.allocate(MessageHeader.Length)
|
val b = ByteBuffer.allocate(MessageHeader.Length)
|
||||||
val bodyBytes = body.write
|
|
||||||
|
|
||||||
val versionAndType = (MessageHeader.Version << 12) | MessageType
|
val versionAndType = (MessageHeader.Version << 12) | MessageType
|
||||||
BufferUtils.putUnsignedShort(b, versionAndType)
|
BufferUtils.putUnsignedShort(b, versionAndType)
|
||||||
BufferUtils.putUnsignedByte(b, HopLimit)
|
BufferUtils.putUnsignedByte(b, HopLimit)
|
||||||
BufferUtils.putUnsignedByte(b, HopCount)
|
BufferUtils.putUnsignedByte(b, HopCount)
|
||||||
|
|
||||||
BufferUtils.putUnsignedInt(b, MessageHeader.Length + bodyBytes.length)
|
BufferUtils.putUnsignedInt(b, MessageHeader.Length + contentLength)
|
||||||
b.putInt((Time.getTime / 1000).toInt)
|
b.putInt((Time.getTime / 1000).toInt)
|
||||||
b.put(Origin.Bytes)
|
b.put(Origin.Bytes)
|
||||||
b.put(Target.Bytes)
|
b.put(Target.Bytes)
|
||||||
|
@ -77,7 +76,7 @@ class MessageHeader(val MessageType: Int,
|
||||||
BufferUtils.putUnsignedByte(b, Metric)
|
BufferUtils.putUnsignedByte(b, Metric)
|
||||||
BufferUtils.putUnsignedByte(b, 0)
|
BufferUtils.putUnsignedByte(b, 0)
|
||||||
|
|
||||||
b.array() ++ bodyBytes
|
b.array()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def equals(a: Any): Boolean = a match {
|
override def equals(a: Any): Boolean = a match {
|
||||||
|
@ -89,9 +88,8 @@ class MessageHeader(val MessageType: Int,
|
||||||
Target == o.Target &&
|
Target == o.Target &&
|
||||||
SequenceNumber == o.SequenceNumber &&
|
SequenceNumber == o.SequenceNumber &&
|
||||||
Metric == o.Metric &&
|
Metric == o.Metric &&
|
||||||
// Don't compare length as it may be unknown (when header was just created without a body).
|
|
||||||
//Length == o.Length &&
|
|
||||||
HopCount == o.HopCount
|
HopCount == o.HopCount
|
||||||
|
// Don't compare length as it may be unknown (when header was just created without a body).
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
object RequestAddContact {
|
||||||
|
|
||||||
|
val Type = 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs [[RequestAddContact]] instance from byte array.
|
||||||
|
*/
|
||||||
|
def read(array: Array[Byte]): RequestAddContact = {
|
||||||
|
new RequestAddContact()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sent when the user initiates adding another device as a contact.
|
||||||
|
*/
|
||||||
|
class RequestAddContact extends MessageBody {
|
||||||
|
|
||||||
|
override def Type = RequestAddContact.Type
|
||||||
|
|
||||||
|
override def write: Array[Byte] = {
|
||||||
|
val b = ByteBuffer.allocate(length)
|
||||||
|
b.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def toString = "RequestAddContact()"
|
||||||
|
|
||||||
|
override def length = 4
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
import com.nutomic.ensichat.util.BufferUtils
|
||||||
|
|
||||||
|
object ResultAddContact {
|
||||||
|
|
||||||
|
val Type = 5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs [[ResultAddContact]] instance from byte array.
|
||||||
|
*/
|
||||||
|
def read(array: Array[Byte]): ResultAddContact = {
|
||||||
|
val b = ByteBuffer.wrap(array)
|
||||||
|
val first = BufferUtils.getUnsignedByte(b)
|
||||||
|
val accepted = (first & 0x80) != 0
|
||||||
|
new ResultAddContact(accepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the result of a [[RequestAddContact]] message.
|
||||||
|
*/
|
||||||
|
class ResultAddContact(val Accepted: Boolean) extends MessageBody {
|
||||||
|
|
||||||
|
override def Type = ResultAddContact.Type
|
||||||
|
|
||||||
|
override def write: Array[Byte] = {
|
||||||
|
val b = ByteBuffer.allocate(length)
|
||||||
|
BufferUtils.putUnsignedByte(b, if (Accepted) 0x80 else 0)
|
||||||
|
(0 to 1).foreach(_ => BufferUtils.putUnsignedByte(b, 0))
|
||||||
|
b.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def toString = "ResultAddContact(Accepted=" + Accepted + ")"
|
||||||
|
|
||||||
|
override def length = 4
|
||||||
|
|
||||||
|
}
|
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala
Normal file
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package com.nutomic.ensichat.aodvv2
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
import com.nutomic.ensichat.util.BufferUtils
|
||||||
|
|
||||||
|
object Text {
|
||||||
|
|
||||||
|
val Type = 6
|
||||||
|
|
||||||
|
val Charset = "UTF-8"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs [[Text]] instance from byte array.
|
||||||
|
*/
|
||||||
|
def read(array: Array[Byte]): Text = {
|
||||||
|
val b = ByteBuffer.wrap(array)
|
||||||
|
val length = BufferUtils.getUnsignedInt(b).toInt
|
||||||
|
val bytes = new Array[Byte](length)
|
||||||
|
b.get(bytes, 0, length)
|
||||||
|
new Text(new String(bytes, Text.Charset))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a plain text message.
|
||||||
|
*/
|
||||||
|
class Text(val text: String) extends MessageBody {
|
||||||
|
|
||||||
|
override def Type = Text.Type
|
||||||
|
|
||||||
|
override def write: Array[Byte] = {
|
||||||
|
val bytes = text.getBytes(Text.Charset)
|
||||||
|
val b = ByteBuffer.allocate(4 + bytes.length)
|
||||||
|
BufferUtils.putUnsignedInt(b, bytes.length)
|
||||||
|
b.put(bytes)
|
||||||
|
b.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def equals(a: Any): Boolean = a match {
|
||||||
|
case o: Text => text == o.text
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
override def toString = "Text(" + text + ")"
|
||||||
|
|
||||||
|
override def length = write.length
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package com.nutomic.ensichat.bluetooth
|
package com.nutomic.ensichat.bluetooth
|
||||||
|
|
||||||
import java.util.{Date, UUID}
|
import java.util.UUID
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
|
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
|
||||||
|
@ -14,7 +14,6 @@ import com.nutomic.ensichat.aodvv2._
|
||||||
import com.nutomic.ensichat.bluetooth.ChatService.{OnMessageReceivedListener, OnNearbyContactsChangedListener}
|
import com.nutomic.ensichat.bluetooth.ChatService.{OnMessageReceivedListener, OnNearbyContactsChangedListener}
|
||||||
import com.nutomic.ensichat.messages._
|
import com.nutomic.ensichat.messages._
|
||||||
import com.nutomic.ensichat.util.Database
|
import com.nutomic.ensichat.util.Database
|
||||||
import org.msgpack.ScalaMessagePack
|
|
||||||
|
|
||||||
import scala.collection.SortedSet
|
import scala.collection.SortedSet
|
||||||
import scala.collection.immutable.{HashMap, HashSet, TreeSet}
|
import scala.collection.immutable.{HashMap, HashSet, TreeSet}
|
||||||
|
@ -77,6 +76,8 @@ class ChatService extends Service {
|
||||||
|
|
||||||
private lazy val Crypto = new Crypto(this)
|
private lazy val Crypto = new Crypto(this)
|
||||||
|
|
||||||
|
private var discovered = Set[Device]()
|
||||||
|
|
||||||
private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]()
|
private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,6 +90,8 @@ class ChatService extends Service {
|
||||||
|
|
||||||
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
|
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
|
||||||
registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
||||||
|
registerReceiver(DiscoveryFinishedReceiver,
|
||||||
|
new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
|
||||||
if (bluetoothAdapter.isEnabled) {
|
if (bluetoothAdapter.isEnabled) {
|
||||||
startBluetoothConnections()
|
startBluetoothConnections()
|
||||||
}
|
}
|
||||||
|
@ -116,6 +119,7 @@ class ChatService extends Service {
|
||||||
cancelDiscovery = true
|
cancelDiscovery = true
|
||||||
unregisterReceiver(DeviceDiscoveredReceiver)
|
unregisterReceiver(DeviceDiscoveredReceiver)
|
||||||
unregisterReceiver(BluetoothStateReceiver)
|
unregisterReceiver(BluetoothStateReceiver)
|
||||||
|
unregisterReceiver(DiscoveryFinishedReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,12 +130,12 @@ class ChatService extends Service {
|
||||||
return
|
return
|
||||||
|
|
||||||
if (!bluetoothAdapter.isDiscovering) {
|
if (!bluetoothAdapter.isDiscovering) {
|
||||||
Log.v(Tag, "Running discovery")
|
Log.v(Tag, "Starting discovery")
|
||||||
bluetoothAdapter.startDiscovery()
|
bluetoothAdapter.startDiscovery()
|
||||||
}
|
}
|
||||||
|
|
||||||
val scanInterval = PreferenceManager.getDefaultSharedPreferences(this)
|
val scanInterval = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
.getString("scan_interval_seconds", "5").toInt * 1000
|
.getString("scan_interval_seconds", "15").toInt * 1000
|
||||||
MainHandler.postDelayed(new Runnable {
|
MainHandler.postDelayed(new Runnable {
|
||||||
override def run(): Unit = discover()
|
override def run(): Unit = discover()
|
||||||
}, scanInterval)
|
}, scanInterval)
|
||||||
|
@ -142,10 +146,21 @@ class ChatService extends Service {
|
||||||
*/
|
*/
|
||||||
private val DeviceDiscoveredReceiver = new BroadcastReceiver() {
|
private val DeviceDiscoveredReceiver = new BroadcastReceiver() {
|
||||||
override def onReceive(context: Context, intent: Intent) {
|
override def onReceive(context: Context, intent: Intent) {
|
||||||
val device: Device =
|
discovered += new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
|
||||||
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
|
}
|
||||||
devices += (device.Id -> device)
|
}
|
||||||
new ConnectThread(device, onConnectionChanged).start()
|
|
||||||
|
/**
|
||||||
|
* Iniates the actual connection to discovered devices.
|
||||||
|
*/
|
||||||
|
private val DiscoveryFinishedReceiver = new BroadcastReceiver() {
|
||||||
|
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||||
|
discovered.filterNot(d => connections.keySet.contains(d.Id))
|
||||||
|
.foreach { d =>
|
||||||
|
new ConnectThread(d, onConnectionChanged).start()
|
||||||
|
devices += (d.Id -> d)
|
||||||
|
}
|
||||||
|
discovered = Set[Device]()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +215,7 @@ class ChatService extends Service {
|
||||||
def onConnectionChanged(device: Device, socket: BluetoothSocket): Unit = {
|
def onConnectionChanged(device: Device, socket: BluetoothSocket): Unit = {
|
||||||
devices += (device.Id -> device)
|
devices += (device.Id -> device)
|
||||||
|
|
||||||
if (device.Connected) {
|
if (device.Connected && !connections.keySet.contains(device.Id)) {
|
||||||
connections += (device.Id ->
|
connections += (device.Id ->
|
||||||
new TransferThread(device, socket, this, Crypto, onReceiveMessage))
|
new TransferThread(device, socket, this, Crypto, onReceiveMessage))
|
||||||
connections(device.Id).start()
|
connections(device.Id).start()
|
||||||
|
@ -219,79 +234,75 @@ class ChatService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends message to the device specified as receiver,
|
* Sends a new message to the given target address.
|
||||||
*/
|
*/
|
||||||
def send(message: Message): Unit = {
|
def sendTo(target: Address, body: MessageBody): Unit = {
|
||||||
assert(message.sender == Crypto.getLocalAddress, "Message must be sent from local device")
|
if (!AddressDeviceMap.containsKey(target)) {
|
||||||
|
Log.w(Tag, "Receiver " + target + " is not connected, ignoring message")
|
||||||
if (!AddressDeviceMap.containsKey(message.receiver)) {
|
|
||||||
Log.w(Tag, "Receiver " + message.receiver + " is not connected, ignoring message")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val header = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit,
|
val header = new MessageHeader(body.Type, MessageHeader.DefaultHopLimit,
|
||||||
new Date(), Crypto.getLocalAddress, message.receiver, 0, 0)
|
Crypto.getLocalAddress, target, 0, 0)
|
||||||
|
|
||||||
val plain = message.write(Crypto.calculateSignature(message))
|
val msg = new Message(header, body)
|
||||||
val (encrypted, key) = Crypto.encrypt(message.receiver, plain)
|
val encrypted = Crypto.encrypt(Crypto.sign(msg))
|
||||||
val packer = new ScalaMessagePack().createBufferPacker()
|
connections.apply(AddressDeviceMap.get(target)).send(encrypted)
|
||||||
packer
|
Database.addMessage(msg)
|
||||||
.write(encrypted)
|
callMessageReceivedListeners(msg)
|
||||||
.write(key)
|
|
||||||
val body = new Data(packer.toByteArray)
|
|
||||||
|
|
||||||
connections.apply(AddressDeviceMap.get(message.receiver)).send(header, body)
|
|
||||||
Database.addMessage(message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the message to database and sends it to registered listeners.
|
* Saves the message to database and sends it to registered listeners.
|
||||||
*
|
*
|
||||||
* If you want to send a new message, use [[send]].
|
* If you want to send a new message, use [[sendTo]].
|
||||||
*
|
*
|
||||||
* Messages must always be sent between local device and a contact.
|
* Messages must always be sent between local device and a contact.
|
||||||
*
|
*
|
||||||
* NOTE: Messages sent from the local node using [[send]] are also passed through this method.
|
* NOTE: Messages sent from the local node using [[sendTo]] are also passed through this method.
|
||||||
*/
|
*/
|
||||||
private def onReceiveMessage(header: MessageHeader, body: MessageBody, device: Device.ID): Unit = {
|
private def onReceiveMessage(message: Message, device: Device.ID): Unit = {
|
||||||
assert(header.Origin != Crypto.getLocalAddress)
|
assert(message.Header.Origin != Crypto.getLocalAddress)
|
||||||
|
|
||||||
body match {
|
message.Body match {
|
||||||
case info: ConnectionInfo =>
|
case info: ConnectionInfo =>
|
||||||
if (header.Origin == Crypto.getLocalAddress)
|
if (message.Header.Origin == Crypto.getLocalAddress)
|
||||||
return
|
return
|
||||||
onNeighborConnected(info, device)
|
onNeighborConnected(info, device)
|
||||||
case data: Data =>
|
case _ =>
|
||||||
val up = new ScalaMessagePack().createBufferUnpacker(data.data)
|
val decrypted = Crypto.decrypt(message)
|
||||||
val encrypted = up.readByteArray()
|
if (!Crypto.verify(decrypted)) {
|
||||||
val key = up.readByteArray()
|
Log.i(Tag, "Dropping message with invalid signature from " + message.Header.Origin)
|
||||||
val (message, signature) = Message.read(Crypto.decrypt(encrypted, key))
|
|
||||||
|
|
||||||
if (!Crypto.isValidSignature(message, signature)) {
|
|
||||||
Log.i(Tag, "Dropping message with invalid signature from " + header.Origin)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.addMessage(message)
|
callMessageReceivedListeners(decrypted)
|
||||||
MainHandler.post(new Runnable {
|
Database.addMessage(decrypted)
|
||||||
override def run(): Unit = {
|
|
||||||
messageListeners.foreach(l =>
|
|
||||||
if (l.get != null)
|
|
||||||
l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
|
|
||||||
else
|
|
||||||
messageListeners -= l)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls all [[OnMessageReceivedListener]]s with the new message.
|
||||||
|
*/
|
||||||
|
private def callMessageReceivedListeners(message: Message): Unit = {
|
||||||
|
MainHandler.post(new Runnable {
|
||||||
|
override def run(): Unit = {
|
||||||
|
messageListeners.foreach(l =>
|
||||||
|
if (l.get != null)
|
||||||
|
l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
|
||||||
|
else
|
||||||
|
messageListeners -= l)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a [[ConnectionInfo]] message from a new neighbor is received.
|
* Called when a [[ConnectionInfo]] message from a new neighbor is received.
|
||||||
*/
|
*/
|
||||||
private def onNeighborConnected(info: ConnectionInfo, device: Device.ID): Unit = {
|
private def onNeighborConnected(info: ConnectionInfo, device: Device.ID): Unit = {
|
||||||
val sender = Crypto.calculateAddress(info.key)
|
val sender = Crypto.calculateAddress(info.key)
|
||||||
if (sender == Address.Broadcast || sender == Address.Null) {
|
if (sender == Address.Broadcast || sender == Address.Null) {
|
||||||
connections(device).close()
|
Log.i(Tag, "Received ConnectionInfo message with invalid sender " + sender + ", ignoring")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
|
||||||
Socket.connect()
|
Socket.connect()
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.w(Tag, "Failed to connect to " + device.toString, e)
|
Log.v(Tag, "Failed to connect to " + device.toString, e)
|
||||||
try {
|
try {
|
||||||
Socket.close()
|
Socket.close()
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
|
||||||
}
|
}
|
||||||
|
|
||||||
val device: Device = new Device(socket.getRemoteDevice, true)
|
val device: Device = new Device(socket.getRemoteDevice, true)
|
||||||
|
Log.i(Tag, "Incoming connection from " + device.toString)
|
||||||
onConnected(device, socket)
|
onConnected(device, socket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.nutomic.ensichat.bluetooth
|
package com.nutomic.ensichat.bluetooth
|
||||||
|
|
||||||
import java.io._
|
import java.io._
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
@ -18,7 +17,7 @@ import com.nutomic.ensichat.messages.Crypto
|
||||||
* @param onReceive Called when a message was received from the other device.
|
* @param onReceive Called when a message was received from the other device.
|
||||||
*/
|
*/
|
||||||
class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService,
|
class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService,
|
||||||
crypto: Crypto, onReceive: (MessageHeader, MessageBody, Device.ID) => Unit)
|
crypto: Crypto, onReceive: (Message, Device.ID) => Unit)
|
||||||
extends Thread {
|
extends Thread {
|
||||||
|
|
||||||
private val Tag: String = "TransferThread"
|
private val Tag: String = "TransferThread"
|
||||||
|
@ -44,26 +43,16 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
Log.i(Tag, "Starting data transfer with " + device.toString)
|
Log.i(Tag, "Starting data transfer with " + device.toString)
|
||||||
|
|
||||||
send(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit, new Date(), Address.Null,
|
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit,
|
||||||
Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))
|
Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||||
|
|
||||||
while (socket.isConnected) {
|
while (socket.isConnected) {
|
||||||
try {
|
try {
|
||||||
val headerBytes = new Array[Byte](MessageHeader.Length)
|
if (InStream.available() > 0) {
|
||||||
InStream.read(headerBytes, 0, MessageHeader.Length)
|
val msg = Message.read(InStream)
|
||||||
val header = MessageHeader.read(headerBytes)
|
|
||||||
val bodyLength = (header.Length - MessageHeader.Length).toInt
|
|
||||||
|
|
||||||
val bodyBytes = new Array[Byte](bodyLength)
|
onReceive(msg, device.Id)
|
||||||
InStream.read(bodyBytes, 0, bodyLength)
|
}
|
||||||
|
|
||||||
val body =
|
|
||||||
header.MessageType match {
|
|
||||||
case ConnectionInfo.Type => ConnectionInfo.read(bodyBytes)
|
|
||||||
case Data.Type => Data.read(bodyBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
onReceive(header, body, device.Id)
|
|
||||||
} catch {
|
} catch {
|
||||||
case e: RuntimeException =>
|
case e: RuntimeException =>
|
||||||
Log.i(Tag, "Received invalid message", e)
|
Log.i(Tag, "Received invalid message", e)
|
||||||
|
@ -73,11 +62,12 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
|
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
|
||||||
|
Log.i(Tag, "Neighbor " + device + " has disconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
def send(header: MessageHeader, body: MessageBody): Unit = {
|
def send(msg: Message): Unit = {
|
||||||
try {
|
try {
|
||||||
OutStream.write(header.write(body))
|
OutStream.write(msg.write)
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException => Log.e(Tag, "Failed to write message", e)
|
case e: IOException => Log.e(Tag, "Failed to write message", e)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +75,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
|
||||||
|
|
||||||
def close(): Unit = {
|
def close(): Unit = {
|
||||||
try {
|
try {
|
||||||
|
Log.i(Tag, "Closing connection to " + device)
|
||||||
socket.close()
|
socket.close()
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException => Log.e(Tag, "Failed to close socket", e);
|
case e: IOException => Log.e(Tag, "Failed to close socket", e);
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.nutomic.ensichat.fragments
|
package com.nutomic.ensichat.fragments
|
||||||
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
import android.app.ListFragment
|
import android.app.ListFragment
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View.OnClickListener
|
import android.view.View.OnClickListener
|
||||||
|
@ -11,10 +9,9 @@ import android.widget.TextView.OnEditorActionListener
|
||||||
import android.widget._
|
import android.widget._
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.activities.EnsiChatActivity
|
import com.nutomic.ensichat.activities.EnsiChatActivity
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
import com.nutomic.ensichat.aodvv2.{Address, Message, Text}
|
||||||
import com.nutomic.ensichat.bluetooth.ChatService
|
import com.nutomic.ensichat.bluetooth.ChatService
|
||||||
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||||
import com.nutomic.ensichat.messages.{Crypto, Message, TextMessage}
|
|
||||||
import com.nutomic.ensichat.util.MessagesAdapter
|
import com.nutomic.ensichat.util.MessagesAdapter
|
||||||
|
|
||||||
import scala.collection.SortedSet
|
import scala.collection.SortedSet
|
||||||
|
@ -43,7 +40,7 @@ class ChatFragment extends ListFragment with OnClickListener
|
||||||
|
|
||||||
private var listView: ListView = _
|
private var listView: ListView = _
|
||||||
|
|
||||||
private var adapter: ArrayAdapter[TextMessage] = _
|
private var adapter: ArrayAdapter[Message] = _
|
||||||
|
|
||||||
override def onActivityCreated(savedInstanceState: Bundle): Unit = {
|
override def onActivityCreated(savedInstanceState: Bundle): Unit = {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
@ -103,10 +100,8 @@ class ChatFragment extends ListFragment with OnClickListener
|
||||||
case R.id.send =>
|
case R.id.send =>
|
||||||
val text = messageText.getText.toString.trim
|
val text = messageText.getText.toString.trim
|
||||||
if (!text.isEmpty) {
|
if (!text.isEmpty) {
|
||||||
val message =
|
val message = new Text(text.toString)
|
||||||
new TextMessage(Crypto.getLocalAddress(getActivity), address, new Date(), text.toString)
|
chatService.sendTo(address, message)
|
||||||
chatService.send(message)
|
|
||||||
adapter.add(message)
|
|
||||||
messageText.getText.clear()
|
messageText.getText.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,9 +110,8 @@ class ChatFragment extends ListFragment with OnClickListener
|
||||||
* Displays new messages in UI.
|
* Displays new messages in UI.
|
||||||
*/
|
*/
|
||||||
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
||||||
messages.filter(m => m.sender == address || m.receiver == address)
|
messages.filter(m => Set(m.Header.Origin, m.Header.Target).contains(address))
|
||||||
.filter(_.isInstanceOf[TextMessage])
|
.foreach(adapter.add)
|
||||||
.foreach(m => adapter.add(m.asInstanceOf[TextMessage]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
import com.nutomic.ensichat.aodvv2._
|
||||||
import com.nutomic.ensichat.messages.Crypto._
|
import com.nutomic.ensichat.messages.Crypto._
|
||||||
import com.nutomic.ensichat.util.PRNGFixes
|
import com.nutomic.ensichat.util.PRNGFixes
|
||||||
|
|
||||||
|
@ -107,6 +107,7 @@ class Crypto(Context: Context) {
|
||||||
*
|
*
|
||||||
* @throws RuntimeException If the key does not exist.
|
* @throws RuntimeException If the key does not exist.
|
||||||
*/
|
*/
|
||||||
|
@throws[RuntimeException]
|
||||||
def getPublicKey(address: Address): PublicKey = {
|
def getPublicKey(address: Address): PublicKey = {
|
||||||
loadKey(address.toString, classOf[PublicKey])
|
loadKey(address.toString, classOf[PublicKey])
|
||||||
}
|
}
|
||||||
|
@ -114,49 +115,32 @@ class Crypto(Context: Context) {
|
||||||
/**
|
/**
|
||||||
* Adds a new public key for a remote device.
|
* Adds a new public key for a remote device.
|
||||||
*
|
*
|
||||||
* If a key for the device already exists, nothing is done.
|
* @throws RuntimeException If a this key
|
||||||
*
|
|
||||||
* @param address The device to which the key belongs.
|
|
||||||
* @param key The new key to add.
|
|
||||||
*/
|
*/
|
||||||
|
@throws[RuntimeException]
|
||||||
def addPublicKey(address: Address, key: PublicKey): Unit = {
|
def addPublicKey(address: Address, key: PublicKey): Unit = {
|
||||||
if (!havePublicKey(address)) {
|
if (havePublicKey(address))
|
||||||
saveKey(address.toString, key)
|
throw new RuntimeException("Already have key for " + address + ", not overwriting")
|
||||||
} else {
|
|
||||||
Log.i(Tag, "Already have key for " + address.toString + ", not overwriting")
|
saveKey(address.toString, key)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
def sign(msg: Message): Message = {
|
||||||
* Checks if the message was properly signed.
|
|
||||||
*
|
|
||||||
* This is done by signing the output of [[Message.write()]] called with an empty signature.
|
|
||||||
*
|
|
||||||
* @param message The message to verify.
|
|
||||||
* @param signature The signature that was sent
|
|
||||||
* @return True if the signature is valid.
|
|
||||||
*/
|
|
||||||
def isValidSignature(message: Message, signature: Array[Byte], key: PublicKey = null): Boolean = {
|
|
||||||
val publicKey =
|
|
||||||
if (key != null) key
|
|
||||||
else loadKey(message.sender.toString, classOf[PublicKey])
|
|
||||||
val sig = Signature.getInstance(SignAlgorithm)
|
|
||||||
sig.initVerify(publicKey)
|
|
||||||
sig.update(message.write(Array[Byte]()))
|
|
||||||
sig.verify(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a cryptographic signature for the given message (using local private key).
|
|
||||||
*
|
|
||||||
* This is done by signing the output of [[Message.write()]] called with an empty signature.
|
|
||||||
*/
|
|
||||||
def calculateSignature(message: Message): Array[Byte] = {
|
|
||||||
val sig = Signature.getInstance(SignAlgorithm)
|
val sig = Signature.getInstance(SignAlgorithm)
|
||||||
val key = loadKey(PrivateKeyAlias, classOf[PrivateKey])
|
val key = loadKey(PrivateKeyAlias, classOf[PrivateKey])
|
||||||
sig.initSign(key)
|
sig.initSign(key)
|
||||||
sig.update(message.write(Array[Byte]()))
|
sig.update(msg.Body.write)
|
||||||
sig.sign
|
new Message(msg.Header, new CryptoData(Option(sig.sign), None), msg.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify(msg: Message, key: PublicKey = null): Boolean = {
|
||||||
|
val publicKey =
|
||||||
|
if (key != null) key
|
||||||
|
else loadKey(msg.Header.Origin.toString, classOf[PublicKey])
|
||||||
|
val sig = Signature.getInstance(SignAlgorithm)
|
||||||
|
sig.initVerify(publicKey)
|
||||||
|
sig.update(msg.Body.write)
|
||||||
|
sig.verify(msg.Crypto.Signature.get)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -239,50 +223,42 @@ class Crypto(Context: Context) {
|
||||||
*/
|
*/
|
||||||
private def keyFolder = new File(Context.getFilesDir, "keys")
|
private def keyFolder = new File(Context.getFilesDir, "keys")
|
||||||
|
|
||||||
/**
|
def encrypt(msg: Message, key: PublicKey = null): Message = {
|
||||||
* Encrypts data for the given receiver.
|
assert(msg.Crypto.Signature.isDefined, "Message must be signed before encryption")
|
||||||
*
|
|
||||||
* @param receiver The device that should be able to decrypt this message.
|
|
||||||
* @param data The message to encrypt.
|
|
||||||
* @param key Optional RSA public key to use for encryption.
|
|
||||||
* @return Pair of AES encrypted data and RSA encrypted AES key.
|
|
||||||
*/
|
|
||||||
def encrypt(receiver: Address, data: Array[Byte], key: PublicKey = null):
|
|
||||||
(Array[Byte], Array[Byte]) = {
|
|
||||||
// Symmetric encryption of data
|
// Symmetric encryption of data
|
||||||
val secretKey = makeSecretKey()
|
val secretKey = makeSecretKey()
|
||||||
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
|
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
|
||||||
symmetricCipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
symmetricCipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||||
val encryptedData = copyThroughCipher(symmetricCipher, data)
|
val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.Body.write))
|
||||||
|
|
||||||
// Asymmetric encryption of secret key
|
// Asymmetric encryption of secret key
|
||||||
val publicKey =
|
val publicKey =
|
||||||
if (key != null) key
|
if (key != null) key
|
||||||
else loadKey(receiver.toString, classOf[PublicKey])
|
else loadKey(msg.Header.Target.toString, classOf[PublicKey])
|
||||||
val asymmetricCipher = Cipher.getInstance(KeyAlgorithm)
|
val asymmetricCipher = Cipher.getInstance(KeyAlgorithm)
|
||||||
asymmetricCipher.init(Cipher.WRAP_MODE, publicKey)
|
asymmetricCipher.init(Cipher.WRAP_MODE, publicKey)
|
||||||
|
|
||||||
(encryptedData, asymmetricCipher.wrap(secretKey))
|
new Message(msg.Header,
|
||||||
|
new CryptoData(msg.Crypto.Signature, Option(asymmetricCipher.wrap(secretKey))), encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
def decrypt(msg: Message): Message = {
|
||||||
* Decrypts the output of [[encrypt]].
|
|
||||||
*
|
|
||||||
* @param data The AES encrypted data to decrypt.
|
|
||||||
* @param key The RSA encrypted AES key used to encrypt data.
|
|
||||||
* @return The plain text data.
|
|
||||||
*/
|
|
||||||
def decrypt(data: Array[Byte], key: Array[Byte]): Array[Byte] = {
|
|
||||||
// Asymmetric decryption of secret key
|
// Asymmetric decryption of secret key
|
||||||
val asymmetricCipher = Cipher.getInstance(KeyAlgorithm)
|
val asymmetricCipher = Cipher.getInstance(KeyAlgorithm)
|
||||||
asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey]))
|
asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey]))
|
||||||
val secretKey = asymmetricCipher.unwrap(key, SymmetricKeyAlgorithm, Cipher.SECRET_KEY)
|
val key = asymmetricCipher.unwrap(msg.Crypto.Key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY)
|
||||||
|
|
||||||
// Symmetric decryption of data
|
// Symmetric decryption of data
|
||||||
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
|
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
|
||||||
symmetricCipher.init(Cipher.DECRYPT_MODE, secretKey)
|
symmetricCipher.init(Cipher.DECRYPT_MODE, key)
|
||||||
val dec = copyThroughCipher(symmetricCipher, data)
|
val decryped = copyThroughCipher(symmetricCipher, msg.Body.asInstanceOf[EncryptedBody].Data)
|
||||||
dec
|
val body = msg.Header.MessageType match {
|
||||||
|
case RequestAddContact.Type => RequestAddContact.read(decryped)
|
||||||
|
case ResultAddContact.Type => ResultAddContact.read(decryped)
|
||||||
|
case Text.Type => Text.read(decryped)
|
||||||
|
}
|
||||||
|
new Message(msg.Header, msg.Crypto, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
package com.nutomic.ensichat.messages
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.{Date, Objects}
|
|
||||||
|
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
|
||||||
import org.msgpack.ScalaMessagePack
|
|
||||||
import org.msgpack.packer.Packer
|
|
||||||
|
|
||||||
object Message {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Types of messages that can be transfered.
|
|
||||||
*
|
|
||||||
* There must be one type for each implementation and vice versa.
|
|
||||||
*/
|
|
||||||
object Type {
|
|
||||||
val Text = 1
|
|
||||||
val RequestAddContact = 2
|
|
||||||
val ResultAddContact = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Orders messages by date, oldest messages first.
|
|
||||||
*/
|
|
||||||
val Ordering = new Ordering[Message] {
|
|
||||||
override def compare(m1: Message, m2: Message) = m1.date.compareTo(m2.date)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a byte array that was written by [[Message.write]] into the correct
|
|
||||||
* message implementation..
|
|
||||||
*
|
|
||||||
* @return Deserialized message and sits signature.
|
|
||||||
*/
|
|
||||||
def read(bytes: Array[Byte]): (Message, Array[Byte]) = {
|
|
||||||
val up = new ScalaMessagePack().createBufferUnpacker(bytes)
|
|
||||||
|
|
||||||
@throws[IOException]("If the message can't be parsed")
|
|
||||||
@throws[RuntimeException]("If the message has an unknown type")
|
|
||||||
val messageType = up.readInt()
|
|
||||||
val sender = new Address(up.readByteArray())
|
|
||||||
val receiver = new Address(up.readByteArray())
|
|
||||||
val date = new Date(up.readLong())
|
|
||||||
val sig = up.readByteArray()
|
|
||||||
(messageType match {
|
|
||||||
case Type.Text => TextMessage.read(sender, receiver, date, up)
|
|
||||||
case Type.RequestAddContact => RequestAddContactMessage.read(sender, receiver, date, up)
|
|
||||||
case Type.ResultAddContact => ResultAddContactMessage.read(sender, receiver, date, up)
|
|
||||||
case t =>
|
|
||||||
throw new RuntimeException("Received message of unknown type " + t)
|
|
||||||
}, sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message object that can be sent between remote devices.
|
|
||||||
*
|
|
||||||
* @param messageType One of [[Message.Type]].
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
abstract class Message(val messageType: Int) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device where the message was sent from.
|
|
||||||
*/
|
|
||||||
val sender: Address
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device the message is addressed to.
|
|
||||||
*/
|
|
||||||
val receiver: Address
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Timestamp of message creation.
|
|
||||||
*/
|
|
||||||
val date: Date
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes this message and the given signature into byte array.
|
|
||||||
*
|
|
||||||
* Signature may not be null, but can be an empty array.
|
|
||||||
*/
|
|
||||||
def write(signature: Array[Byte]): Array[Byte] = {
|
|
||||||
val packer = new ScalaMessagePack().createBufferPacker()
|
|
||||||
packer.write(messageType)
|
|
||||||
.write(sender.Bytes)
|
|
||||||
.write(receiver.Bytes)
|
|
||||||
.write(date.getTime)
|
|
||||||
.write(signature)
|
|
||||||
doWrite(packer)
|
|
||||||
packer.toByteArray
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes any extra data for implementing classes.
|
|
||||||
*/
|
|
||||||
protected def doWrite(packer: Packer): Unit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if objects are equal.
|
|
||||||
*
|
|
||||||
* Implementations must provide their own implementation to check the result of this
|
|
||||||
* function and their own data.
|
|
||||||
*/
|
|
||||||
override def equals(a: Any): Boolean = a match {
|
|
||||||
case o: Message => sender == o.sender && receiver == o.receiver && date == o.date
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a hash code for this object.
|
|
||||||
*
|
|
||||||
* Implementations must provide their own implementation to check the result of this
|
|
||||||
* function and their own data.
|
|
||||||
*/
|
|
||||||
override def hashCode: Int = Objects.hash(sender, receiver, date)
|
|
||||||
|
|
||||||
override def toString: String
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package com.nutomic.ensichat.messages
|
|
||||||
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
import com.nutomic.ensichat.activities.AddContactsActivity
|
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
|
||||||
import com.nutomic.ensichat.messages.Message._
|
|
||||||
import org.msgpack.packer.Packer
|
|
||||||
import org.msgpack.unpacker.Unpacker
|
|
||||||
|
|
||||||
object RequestAddContactMessage {
|
|
||||||
|
|
||||||
def read(sender: Address, receiver: Address, date: Date, up: Unpacker) =
|
|
||||||
new RequestAddContactMessage(sender, receiver, date)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message sent by [[AddContactsActivity]] to notify a device that it should be added as a contact.
|
|
||||||
*/
|
|
||||||
class RequestAddContactMessage(override val sender: Address, override val receiver: Address,
|
|
||||||
override val date: Date) extends Message(Type.RequestAddContact) {
|
|
||||||
|
|
||||||
override def doWrite(packer: Packer) = {
|
|
||||||
}
|
|
||||||
|
|
||||||
override def toString = "RequestAddContactMessage(" + sender.toString + ", " + receiver.toString +
|
|
||||||
", " + date.toString + ")"
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package com.nutomic.ensichat.messages
|
|
||||||
|
|
||||||
import java.util.{Date, Objects}
|
|
||||||
|
|
||||||
import com.nutomic.ensichat.activities.AddContactsActivity
|
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
|
||||||
import com.nutomic.ensichat.messages.Message._
|
|
||||||
import org.msgpack.packer.Packer
|
|
||||||
import org.msgpack.unpacker.Unpacker
|
|
||||||
|
|
||||||
object ResultAddContactMessage {
|
|
||||||
|
|
||||||
def read(sender: Address, receiver: Address, date: Date, up: Unpacker) =
|
|
||||||
new ResultAddContactMessage(sender, receiver, date, up.readBoolean())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message sent by [[AddContactsActivity]] to tell a device whether the user confirmed adding it
|
|
||||||
* to contacts.
|
|
||||||
*/
|
|
||||||
class ResultAddContactMessage(override val sender: Address, override val receiver: Address,
|
|
||||||
override val date: Date, val Accepted: Boolean)
|
|
||||||
extends Message(Type.ResultAddContact) {
|
|
||||||
|
|
||||||
override def doWrite(packer: Packer) = packer.write(Accepted)
|
|
||||||
|
|
||||||
override def equals(a: Any) =
|
|
||||||
super.equals(a) && a.asInstanceOf[ResultAddContactMessage].Accepted == Accepted
|
|
||||||
|
|
||||||
override def hashCode =
|
|
||||||
Objects.hash(super.hashCode: java.lang.Integer, Accepted: java.lang.Boolean)
|
|
||||||
|
|
||||||
override def toString = "ResultAddContactMessage(" + sender.toString + ", " + receiver.toString +
|
|
||||||
", " + date.toString + ", " + Accepted + ")"
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package com.nutomic.ensichat.messages
|
|
||||||
|
|
||||||
import java.util.{Date, Objects}
|
|
||||||
|
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
|
||||||
import com.nutomic.ensichat.messages.Message._
|
|
||||||
import org.msgpack.packer.Packer
|
|
||||||
import org.msgpack.unpacker.Unpacker
|
|
||||||
|
|
||||||
object TextMessage {
|
|
||||||
|
|
||||||
def read(sender: Address, receiver: Address, date: Date, up: Unpacker): TextMessage =
|
|
||||||
new TextMessage(sender, receiver, date, up.readString())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message that contains text.
|
|
||||||
*/
|
|
||||||
class TextMessage(override val sender: Address, override val receiver: Address,
|
|
||||||
override val date: Date, val text: String) extends Message(Type.Text) {
|
|
||||||
|
|
||||||
override def doWrite(packer: Packer) = packer.write(text)
|
|
||||||
|
|
||||||
override def equals(a: Any) = super.equals(a) && a.asInstanceOf[TextMessage].text == text
|
|
||||||
|
|
||||||
override def hashCode = Objects.hash(super.hashCode: java.lang.Integer, text)
|
|
||||||
|
|
||||||
override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString +
|
|
||||||
", " + date.toString + ", " + text + ")"
|
|
||||||
|
|
||||||
}
|
|
|
@ -2,6 +2,9 @@ package com.nutomic.ensichat.util
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides various helper methods for [[ByteBuffer]].
|
||||||
|
*/
|
||||||
object BufferUtils {
|
object BufferUtils {
|
||||||
|
|
||||||
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
|
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
|
||||||
|
@ -25,6 +28,6 @@ object BufferUtils {
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
|
|
||||||
def toString(bb: ByteBuffer)= bb.array().slice(0, 4).map("%02X" format _).mkString
|
def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,8 +4,7 @@ import java.util.Date
|
||||||
|
|
||||||
import android.content.{ContentValues, Context}
|
import android.content.{ContentValues, Context}
|
||||||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
import com.nutomic.ensichat.aodvv2._
|
||||||
import com.nutomic.ensichat.messages._
|
|
||||||
|
|
||||||
import scala.collection.SortedSet
|
import scala.collection.SortedSet
|
||||||
import scala.collection.immutable.TreeSet
|
import scala.collection.immutable.TreeSet
|
||||||
|
@ -18,8 +17,8 @@ object Database {
|
||||||
|
|
||||||
private val CreateMessagesTable = "CREATE TABLE messages(" +
|
private val CreateMessagesTable = "CREATE TABLE messages(" +
|
||||||
"_id integer primary key autoincrement," +
|
"_id integer primary key autoincrement," +
|
||||||
"sender text not null," +
|
"origin text not null," +
|
||||||
"receiver text not null," +
|
"target text not null," +
|
||||||
"text text not null," +
|
"text text not null," +
|
||||||
"date integer not null);" // Unix timestamp of message.
|
"date integer not null);" // Unix timestamp of message.
|
||||||
|
|
||||||
|
@ -35,8 +34,6 @@ object Database {
|
||||||
class Database(context: Context) extends SQLiteOpenHelper(context, Database.DatabaseName,
|
class Database(context: Context) extends SQLiteOpenHelper(context, Database.DatabaseName,
|
||||||
null, Database.DatabaseVersion) {
|
null, Database.DatabaseVersion) {
|
||||||
|
|
||||||
private val Tag = "MessageStore"
|
|
||||||
|
|
||||||
private var contactsUpdatedListeners = Set[() => Unit]()
|
private var contactsUpdatedListeners = Set[() => Unit]()
|
||||||
|
|
||||||
override def onCreate(db: SQLiteDatabase): Unit = {
|
override def onCreate(db: SQLiteDatabase): Unit = {
|
||||||
|
@ -49,17 +46,21 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
|
||||||
*/
|
*/
|
||||||
def getMessages(address: Address, count: Int): SortedSet[Message] = {
|
def getMessages(address: Address, count: Int): SortedSet[Message] = {
|
||||||
val c = getReadableDatabase.query(true,
|
val c = getReadableDatabase.query(true,
|
||||||
"messages", Array("sender", "receiver", "text", "date"),
|
"messages", Array("origin", "target", "text", "date"),
|
||||||
"sender = ? OR receiver = ?", Array(address.toString, address.toString),
|
"origin = ? OR target = ?", Array(address.toString, address.toString),
|
||||||
null, null, "date DESC", count.toString)
|
null, null, "date DESC", count.toString)
|
||||||
var messages = new TreeSet[Message]()(Message.Ordering)
|
var messages = new TreeSet[Message]()(Message.Ordering)
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
val m = new TextMessage(
|
val header = new MessageHeader(
|
||||||
new Address(c.getString(c.getColumnIndex("sender"))),
|
Text.Type,
|
||||||
new Address(c.getString(c.getColumnIndex("receiver"))),
|
-1,
|
||||||
new Date(c.getLong(c.getColumnIndex("date"))),
|
new Address(c.getString(c.getColumnIndex("origin"))),
|
||||||
new String(c.getString(c.getColumnIndex ("text"))))
|
new Address(c.getString(c.getColumnIndex("target"))),
|
||||||
messages += m
|
-1,
|
||||||
|
-1,
|
||||||
|
new Date(c.getLong(c.getColumnIndex("date"))))
|
||||||
|
val body = new Text(new String(c.getString(c.getColumnIndex ("text"))))
|
||||||
|
messages += new Message(header, body)
|
||||||
}
|
}
|
||||||
c.close()
|
c.close()
|
||||||
messages
|
messages
|
||||||
|
@ -68,16 +69,16 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
|
||||||
/**
|
/**
|
||||||
* Inserts the given new message into the database.
|
* Inserts the given new message into the database.
|
||||||
*/
|
*/
|
||||||
def addMessage(message: Message): Unit = message match {
|
def addMessage(message: Message): Unit = message.Body match {
|
||||||
case msg: TextMessage =>
|
case msg: Text =>
|
||||||
val cv = new ContentValues()
|
val cv = new ContentValues()
|
||||||
cv.put("sender", msg.sender.toString)
|
cv.put("origin", message.Header.Origin.toString)
|
||||||
cv.put("receiver", msg.receiver.toString)
|
cv.put("target", message.Header.Target.toString)
|
||||||
// toString used as workaround for compile error with Long.
|
// toString used as workaround for compile error with Long.
|
||||||
cv.put("date", msg.date.getTime.toString)
|
cv.put("date", message.Header.Time.getTime.toString)
|
||||||
cv.put("text", msg.text)
|
cv.put("text", msg.text)
|
||||||
getWritableDatabase.insert("messages", null, cv)
|
getWritableDatabase.insert("messages", null, cv)
|
||||||
case _: RequestAddContactMessage | _: ResultAddContactMessage =>
|
case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact =>
|
||||||
// Never stored.
|
// Never stored.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,13 @@ import android.content.Context
|
||||||
import android.view.{Gravity, View, ViewGroup}
|
import android.view.{Gravity, View, ViewGroup}
|
||||||
import android.widget.{ArrayAdapter, RelativeLayout, TextView}
|
import android.widget.{ArrayAdapter, RelativeLayout, TextView}
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.aodvv2.Address
|
import com.nutomic.ensichat.aodvv2.{Address, Message, Text}
|
||||||
import com.nutomic.ensichat.messages.TextMessage
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays [[TextMessage]]s in ListView.
|
* Displays [[Message]]s in ListView.
|
||||||
*/
|
*/
|
||||||
class MessagesAdapter(context: Context, remoteAddress: Address) extends
|
class MessagesAdapter(context: Context, remoteAddress: Address) extends
|
||||||
ArrayAdapter[TextMessage](context, R.layout.item_message, android.R.id.text1) {
|
ArrayAdapter[Message](context, R.layout.item_message, android.R.id.text1) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Free space to the right/left to a message depending on who sent it, in dip.
|
* Free space to the right/left to a message depending on who sent it, in dip.
|
||||||
|
@ -22,11 +21,11 @@ class MessagesAdapter(context: Context, remoteAddress: Address) extends
|
||||||
val view = super.getView(position, convertView, parent).asInstanceOf[RelativeLayout]
|
val view = super.getView(position, convertView, parent).asInstanceOf[RelativeLayout]
|
||||||
val tv = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
val tv = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||||
|
|
||||||
tv.setText(getItem(position).text)
|
tv.setText(getItem(position).Body.asInstanceOf[Text].text)
|
||||||
|
|
||||||
val lp = new RelativeLayout.LayoutParams(tv.getLayoutParams)
|
val lp = new RelativeLayout.LayoutParams(tv.getLayoutParams)
|
||||||
val margin = (MessageMargin * context.getResources.getDisplayMetrics.density).toInt
|
val margin = (MessageMargin * context.getResources.getDisplayMetrics.density).toInt
|
||||||
if (getItem(position).sender != remoteAddress) {
|
if (getItem(position).Header.Origin != remoteAddress) {
|
||||||
view.setGravity(Gravity.RIGHT)
|
view.setGravity(Gravity.RIGHT)
|
||||||
lp.setMargins(margin, 0, 0, 0)
|
lp.setMargins(margin, 0, 0, 0)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:0.14.2'
|
classpath 'com.android.tools.build:gradle:0.14.2'
|
||||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.5'
|
classpath 'com.github.ben-manes:gradle-versions-plugin:0.6'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
BIN
gradle/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Thu Dec 04 22:57:09 EET 2014
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip
|
164
gradle/gradlew
vendored
Executable file
164
gradle/gradlew
vendored
Executable file
|
@ -0,0 +1,164 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn ( ) {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die ( ) {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||||
|
if $cygwin ; then
|
||||||
|
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >&-
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >&-
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||||
|
function splitJvmOpts() {
|
||||||
|
JVM_OPTS=("$@")
|
||||||
|
}
|
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
gradle/gradlew.bat
vendored
Normal file
90
gradle/gradlew.bat
vendored
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windowz variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
goto execute
|
||||||
|
|
||||||
|
:4NT_args
|
||||||
|
@rem Get arguments from the 4NT Shell from JP Software
|
||||||
|
set CMD_LINE_ARGS=%$
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
11
gradle/local.properties
Normal file
11
gradle/local.properties
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
## This file is automatically generated by Android Studio.
|
||||||
|
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||||
|
#
|
||||||
|
# This file must *NOT* be checked into Version Control Systems,
|
||||||
|
# as it contains information specific to your local configuration.
|
||||||
|
#
|
||||||
|
# Location of the SDK. This is only used by Gradle.
|
||||||
|
# For customization when using a Version Control System, please read the
|
||||||
|
# header note.
|
||||||
|
#Thu Dec 04 22:57:09 EET 2014
|
||||||
|
sdk.dir=/home/felix/software/android-sdk
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +1,6 @@
|
||||||
|
#Tue Dec 09 17:41:26 EET 2014
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=http\://services.gradle.org/distributions/gradle-2.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip
|
||||||
|
|
Reference in a new issue