├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE
├── PROTOCOL.md
├── README.md
├── android
├── .gitignore
├── build.gradle
├── lint.xml
└── src
│ ├── androidTest
│ └── scala
│ │ └── com
│ │ └── nutomic
│ │ └── ensichat
│ │ └── bluetooth
│ │ └── BluetoothInterfaceTest.scala
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── nutomic
│ │ └── ensichat
│ │ └── util
│ │ └── PRNGFixes.java
│ ├── res
│ ├── drawable-hdpi
│ │ ├── ic_action_send_now.png
│ │ ├── ic_add_white_24dp.png
│ │ ├── ic_launcher.png
│ │ ├── ic_person_add_white_24dp.png
│ │ └── ic_qrcode_white_24dp.png
│ ├── drawable-mdpi
│ │ ├── ic_action_send_now.png
│ │ ├── ic_add_white_24dp.png
│ │ ├── ic_launcher.png
│ │ ├── ic_person_add_white_24dp.png
│ │ └── ic_qrcode_white_24dp.png
│ ├── drawable-xhdpi
│ │ ├── ic_action_send_now.png
│ │ ├── ic_add_white_24dp.png
│ │ ├── ic_launcher.png
│ │ ├── ic_person_add_white_24dp.png
│ │ └── ic_qrcode_white_24dp.png
│ ├── drawable-xxhdpi
│ │ ├── ic_action_send_now.png
│ │ ├── ic_add_white_24dp.png
│ │ ├── ic_launcher.png
│ │ ├── ic_person_add_white_24dp.png
│ │ └── ic_qrcode_white_24dp.png
│ ├── drawable-xxxhdpi
│ │ ├── ic_add_white_24dp.png
│ │ ├── ic_launcher.png
│ │ ├── ic_person_add_white_24dp.png
│ │ └── ic_qrcode_white_24dp.png
│ ├── drawable
│ │ └── message_background.xml
│ ├── layout
│ │ ├── activity_connections.xml
│ │ ├── activity_first_start.xml
│ │ ├── activity_main.xml
│ │ ├── fragment_chat.xml
│ │ ├── fragment_contacts.xml
│ │ ├── fragment_identicon.xml
│ │ ├── item_date.xml
│ │ ├── item_message.xml
│ │ └── item_user.xml
│ ├── menu
│ │ ├── connections.xml
│ │ └── main.xml
│ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── style.xml
│ └── xml
│ │ └── settings.xml
│ └── scala
│ └── com
│ └── nutomic
│ └── ensichat
│ ├── App.scala
│ ├── activities
│ ├── ConnectionsActivity.scala
│ ├── EnsichatActivity.scala
│ ├── FirstStartActivity.scala
│ ├── MainActivity.scala
│ └── SettingsActivity.scala
│ ├── bluetooth
│ ├── BluetoothConnectThread.scala
│ ├── BluetoothDevice.scala
│ ├── BluetoothInterface.scala
│ ├── BluetoothListenThread.scala
│ └── BluetoothTransferThread.scala
│ ├── fragments
│ ├── ChatFragment.scala
│ ├── ContactsFragment.scala
│ ├── SettingsFragment.scala
│ └── UserInfoFragment.scala
│ ├── service
│ ├── BootReceiver.scala
│ ├── CallbackHandler.scala
│ ├── ChatService.scala
│ └── NotificationHandler.scala
│ ├── util
│ ├── IdenticonGenerator.scala
│ ├── NetworkChangedReceiver.scala
│ └── SettingsWrapper.scala
│ └── views
│ ├── DatesAdapter.scala
│ ├── MessagesAdapter.scala
│ └── UsersAdapter.scala
├── build.gradle
├── core
├── .gitignore
├── build.gradle
└── src
│ ├── main
│ ├── resources
│ │ └── logback.xml
│ └── scala
│ │ └── com
│ │ └── nutomic
│ │ └── ensichat
│ │ └── core
│ │ ├── ConnectionHandler.scala
│ │ ├── interfaces
│ │ ├── CallbackInterface.scala
│ │ ├── SettingsInterface.scala
│ │ └── TransmissionInterface.scala
│ │ ├── internet
│ │ ├── InternetConnectionThread.scala
│ │ ├── InternetInterface.scala
│ │ └── InternetServerThread.scala
│ │ ├── messages
│ │ ├── Message.scala
│ │ ├── body
│ │ │ ├── ConnectionInfo.scala
│ │ │ ├── CryptoData.scala
│ │ │ ├── EncryptedBody.scala
│ │ │ ├── MessageBody.scala
│ │ │ ├── MessageReceived.scala
│ │ │ ├── PublicKeyReply.scala
│ │ │ ├── PublicKeyRequest.scala
│ │ │ ├── RouteError.scala
│ │ │ ├── RouteReply.scala
│ │ │ ├── RouteRequest.scala
│ │ │ ├── Text.scala
│ │ │ └── UserInfo.scala
│ │ └── header
│ │ │ ├── AbstractHeader.scala
│ │ │ ├── ContentHeader.scala
│ │ │ └── MessageHeader.scala
│ │ ├── routing
│ │ ├── Address.scala
│ │ ├── LocalRoutesInfo.scala
│ │ ├── MessageBuffer.scala
│ │ ├── RouteMessageInfo.scala
│ │ └── Router.scala
│ │ └── util
│ │ ├── BufferUtils.scala
│ │ ├── Crypto.scala
│ │ ├── Database.scala
│ │ ├── FutureHelper.scala
│ │ ├── SeqNumGenerator.scala
│ │ └── User.scala
│ └── test
│ └── scala
│ └── com
│ └── nutomic
│ └── ensichat
│ └── core
│ ├── messages
│ ├── MessageTest.scala
│ ├── body
│ │ ├── ConnectionInfoTest.scala
│ │ ├── RouteErrorTest.scala
│ │ ├── RouteReplyTest.scala
│ │ ├── RouteRequestTest.scala
│ │ └── UserInfoTest.scala
│ └── header
│ │ ├── ContentHeaderTest.scala
│ │ └── MessageHeaderTest.scala
│ ├── routing
│ ├── AddressTest.scala
│ ├── LocalRoutesInfoTest.scala
│ ├── MessageBufferTest.scala
│ ├── RouteMessageInfoTest.scala
│ └── RouterTest.scala
│ └── util
│ ├── CryptoTest.scala
│ ├── DatabaseTest.scala
│ └── UserTest.scala
├── docs
└── bachelor-thesis.pdf
├── gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── graphics
├── ic_launcher.svg
├── screenshot_phone_1.png
├── screenshot_phone_2.png
└── screenshot_phone_3.png
├── integration
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── scala
│ └── com.nutomic.ensichat.integration
│ ├── LocalNode.scala
│ └── Main.scala
├── server
├── .gitignore
├── build.gradle
└── src
│ ├── dist
│ └── etc
│ │ └── linux-systemd
│ │ └── ensichat.service
│ └── main
│ └── scala
│ └── com
│ └── nutomic
│ └── ensichat
│ └── server
│ ├── Config.scala
│ ├── Main.scala
│ └── Settings.scala
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea
4 | /build
5 | *.iml
6 | *.iws
7 | *.ipr
8 | *.apk
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "buildSrc"]
2 | path = buildSrc
3 | url = https://github.com/xelnaga/gradle-android-scala-plugin.git
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 | jdk: oraclejdk8
3 |
4 | # Install Android SDK
5 | android:
6 | components:
7 | - tools
8 | - platform-tools
9 | - build-tools-24.0.2
10 | - android-24
11 | - extra-android-m2repository
12 |
13 | # Cache gradle dependencies
14 | # https://docs.travis-ci.com/user/languages/android/#Caching
15 | before_cache:
16 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
17 | cache:
18 | directories:
19 | - $HOME/.gradle/caches/
20 | - $HOME/.gradle/wrapper/
21 |
22 | env:
23 | - GRADLE_OPTS=-Xmx2048m
24 |
25 | script:
26 | # Lint fails because travis doesn't have platform-tools 24
27 | # https://github.com/travis-ci/travis-ci/issues/6699
28 | #- ./gradlew lint
29 | - ./gradlew core:test
30 | - ./gradlew server:release
31 | - ./gradlew integration:assemble
32 | - ./gradlew android:assembleRelRelease || ./gradlew android:assembleRelRelease
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PROJECT DISCONTINUED
2 | ====================
3 |
4 | Unfortunately, I won't be able to continue development on Ensichat, due to lack of time. I suggest
5 | you give [Briar](https://briarproject.org/) a try instead.
6 |
7 | If you wish to take over maintenance of the project, please contact me.
8 |
9 | Ensichat
10 | ========
11 |
12 | [](https://travis-ci.org/Nutomic/ensichat)
13 | [](https://opensource.org/licenses/MPL-2.0)
14 |
15 | Instant messenger for Android that is fully decentralized, and uses strong end-to-end
16 | encryption. Messages are sent directly between devices via Bluetooth or Internet, without any
17 | central server. Relay nodes are used to ensure message delivery, even if the target node is
18 | offline.
19 |
20 | For details on how Ensichat works, you can check out my [bachelor thesis](docs/bachelor-thesis.pdf), and
21 | read the [protocol definition](PROTOCOL.md).
22 |
23 |
24 |
25 |
26 |
27 | [](https://play.google.com/store/apps/details?id=com.nutomic.ensichat) [](https://f-droid.org/repository/browse/?fdid=com.nutomic.ensichat)
28 |
29 | To set up a server, please follow the [instructions on the wiki](https://github.com/Nutomic/ensichat/wiki/Running-your-own-server).
30 |
31 | Building
32 | --------
33 |
34 | To setup a development environment, just install [Android Studio](https://developer.android.com/sdk/)
35 | and import the project.
36 |
37 | Alternatively, you can use the command line. To create a debug apk, run `./gradlew assembleDevDebug`.
38 | This requires at least Android Lollipop on your development device. If you don't have 5.0 or higher,
39 | you have to use `./gradlew assembleRelDebug`. However, this results in considerably slower
40 | incremental builds. To create a release apk, run `./gradlew assembleRelRelease`.
41 |
42 | Testing
43 | -------
44 |
45 | You can run the unit tests with `./gradlew test`. After connecting an Android device, you can run
46 | the Android tests with `./gradlew connectedDevDebugAndroidTest` (or
47 | `./gradlew connectedRelDebugAndroidTest` if your Android version is lower than 5.0).
48 |
49 | To run integration tests for the core module, use `./gradlew integration:run`. If this fails (or
50 | is very slow), try changing the value of Crypto#PublicKeySize to 512 (in the core module).
51 |
52 | License
53 | -------
54 | The project is licensed under the [MPLv2](LICENSE).
55 |
56 | The launcher icon is based on the [Bubbles Icon](https://www.iconfinder.com/icons/285667/bubbles_icon) created by [Paomedia](https://www.iconfinder.com/paomedia) which is available under [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/).
57 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'jp.leafytree.android-scala'
3 |
4 | dependencies {
5 | compile 'com.android.support:design:24.2.1'
6 | compile 'com.android.support:multidex:1.0.1'
7 | compile 'org.scala-lang:scala-library:2.11.7'
8 | compile 'com.mobsandgeeks:adapter-kit:0.5.3'
9 | compile 'com.google.zxing:android-integration:3.3.0'
10 | compile 'com.google.zxing:core:3.3.0'
11 | compile 'org.slf4j:slf4j-android:1.7.21'
12 | compile project(path: ':core')
13 | androidTestCompile 'com.android.support:multidex-instrumentation:1.0.1',
14 | { exclude module: 'multidex' }
15 | }
16 |
17 | // RtlHardcoded behaviour differs between target API versions. We only care about API 15.
18 | preBuild.doFirst {
19 | android.applicationVariants.each { variant ->
20 | if (variant.name == 'devDebug' || variant.name == 'devRelease') {
21 | println variant.name
22 | android.lintOptions.disable 'RtlHardcoded'
23 | }
24 | }
25 | }
26 |
27 | android {
28 | compileSdkVersion 24
29 | buildToolsVersion "24.0.2"
30 |
31 | defaultConfig {
32 | applicationId "com.nutomic.ensichat"
33 | targetSdkVersion 24
34 | versionCode 17
35 | versionName "0.5.2"
36 | multiDexEnabled true
37 | testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
38 | }
39 |
40 | buildTypes {
41 | debug {
42 | applicationIdSuffix ".debug"
43 | testCoverageEnabled true
44 | }
45 | }
46 |
47 | // Increasing minSdkVersion reduces compilation time for MultiDex.
48 | productFlavors {
49 | dev.minSdkVersion 21
50 | rel.minSdkVersion 15
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/android/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/android/src/androidTest/scala/com/nutomic/ensichat/bluetooth/BluetoothInterfaceTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.bluetooth
2 |
3 | import android.bluetooth.BluetoothAdapter
4 | import android.os.Handler
5 | import android.test.AndroidTestCase
6 |
7 | class BluetoothInterfaceTest extends AndroidTestCase {
8 |
9 | private lazy val btInterface = new BluetoothInterface(getContext, new Handler(), null)
10 |
11 | /**
12 | * Test for issue [[https://github.com/Nutomic/ensichat/issues/3 #3]].
13 | */
14 | def testStartBluetoothOff(): Unit = {
15 | val btAdapter = BluetoothAdapter.getDefaultAdapter
16 | if (btAdapter == null)
17 | return
18 |
19 | btAdapter.disable()
20 | btInterface.create()
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
39 |
40 |
44 |
47 |
48 |
49 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/android/src/main/res/drawable-hdpi/ic_action_send_now.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-hdpi/ic_action_send_now.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-hdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-hdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-hdpi/ic_person_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-hdpi/ic_person_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-hdpi/ic_qrcode_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-hdpi/ic_qrcode_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-mdpi/ic_action_send_now.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-mdpi/ic_action_send_now.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-mdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-mdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-mdpi/ic_person_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-mdpi/ic_person_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-mdpi/ic_qrcode_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-mdpi/ic_qrcode_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/ic_action_send_now.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xhdpi/ic_action_send_now.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/ic_person_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xhdpi/ic_person_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/ic_qrcode_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xhdpi/ic_qrcode_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxhdpi/ic_action_send_now.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxhdpi/ic_action_send_now.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxhdpi/ic_person_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxhdpi/ic_person_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxhdpi/ic_qrcode_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxhdpi/ic_qrcode_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxxhdpi/ic_person_add_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxxhdpi/ic_person_add_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xxxhdpi/ic_qrcode_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/android/src/main/res/drawable-xxxhdpi/ic_qrcode_white_24dp.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable/message_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/activity_connections.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
11 |
12 |
20 |
21 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/activity_first_start.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
15 |
24 |
25 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/fragment_chat.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
15 |
16 |
23 |
24 |
25 |
26 |
30 |
31 |
40 |
41 |
42 |
43 |
47 |
48 |
54 |
55 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/fragment_contacts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
12 |
13 |
24 |
25 |
35 |
36 |
42 |
43 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
61 |
62 |
66 |
67 |
74 |
75 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/fragment_identicon.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
18 |
19 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/item_date.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/item_message.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
17 |
18 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/android/src/main/res/layout/item_user.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
17 |
18 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/android/src/main/res/menu/connections.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #3F51B5
6 | #303F9F
7 | #C5CAE9
8 | #448AFF
9 | #212121
10 | #727272
11 | #FFFFFF
12 | #B6B6B6
13 |
14 |
15 | #f3f2fb
16 |
17 | #F44336
18 | #FFEB3B
19 | #8BC34A
20 |
21 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Ensichat
8 |
9 |
10 |
11 |
12 |
13 | Welcome!
14 |
15 |
16 | Enter your Name:
17 |
18 |
19 | Done
20 |
21 |
22 | Location permission is required to scan for other Bluetooth devices
23 |
24 |
25 |
26 |
27 | Please enable Bluetooth to connect with devices near you
28 |
29 |
30 |
31 |
32 |
33 |
34 | - 1 Connection
35 | - %s Connections
36 |
37 |
38 |
39 | You haven\'t added any contacts yet
40 |
41 |
42 | Share App
43 |
44 |
45 |
46 |
47 | Type a message
48 |
49 |
50 |
51 |
52 |
53 | Connections
54 |
55 |
56 | Searching for Users\nRange: ~10m
57 |
58 |
59 | Do you want to add %1$s as contact?
60 |
61 |
62 | Contact added
63 |
64 |
65 | You have already added %1$s as a contact
66 |
67 | Enter user ID
68 |
69 | Invalid address
70 |
71 | Add Contact
72 |
73 | Scan QR-Code
74 |
75 |
76 |
77 |
78 |
79 | Settings
80 |
81 |
82 | Name
83 |
84 |
85 | Status
86 |
87 |
88 | My Address
89 |
90 |
91 | Scan Interval (seconds)
92 |
93 |
94 | Notification Sounds
95 |
96 |
97 | Servers
98 |
99 |
100 | Report Issue
101 |
102 |
103 | Open the Ensichat issue tracker
104 |
105 | https://github.com/Nutomic/ensichat/issues
106 |
107 |
108 | Version
109 |
110 |
111 |
112 |
113 |
114 | Address: %1$s
115 |
116 | Ensichat User Address
117 |
118 | Address copied to clipboard
119 |
120 |
121 |
122 |
123 |
124 | New message!
125 |
126 |
127 |
128 | - Connected to %1$s device
129 | - Connected to %1$s devices
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/android/src/main/res/values/style.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
16 |
17 |
22 |
23 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/android/src/main/res/xml/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
18 |
19 |
24 |
25 |
28 |
29 |
32 |
35 |
36 |
37 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/App.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat
2 |
3 | import android.support.multidex.MultiDexApplication
4 | import com.nutomic.ensichat.util.PRNGFixes
5 |
6 | class App extends MultiDexApplication {
7 |
8 | override def onCreate(): Unit = {
9 | super.onCreate()
10 | PRNGFixes.apply()
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/activities/EnsichatActivity.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.activities
2 |
3 | import android.content.{ComponentName, Context, Intent, ServiceConnection}
4 | import android.os.{Bundle, IBinder}
5 | import android.support.v7.app.AppCompatActivity
6 | import com.nutomic.ensichat.service.ChatService
7 |
8 | /**
9 | * Connects to [[ChatService]] and provides access to it.
10 | */
11 | class EnsichatActivity extends AppCompatActivity with ServiceConnection {
12 |
13 | private var chatService: Option[ChatService] = None
14 |
15 | private var listeners = Set[() => Unit]()
16 |
17 | /**
18 | * Starts service and connects to it.
19 | */
20 | override def onCreate(savedInstanceState: Bundle): Unit = {
21 | super.onCreate(savedInstanceState)
22 | startService(new Intent(this, classOf[ChatService]))
23 | bindService(new Intent(this, classOf[ChatService]), this, Context.BIND_AUTO_CREATE)
24 | }
25 |
26 | /**
27 | * Unbinds service.
28 | */
29 | override def onDestroy(): Unit = {
30 | super.onDestroy()
31 | unbindService(this)
32 | }
33 |
34 | /**
35 | * Calls all listeners registered with [[runOnServiceConnected]].
36 | *
37 | * Clears the list containing them.
38 | */
39 | override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
40 | val binder = iBinder.asInstanceOf[ChatService.Binder]
41 | chatService = Option(binder.service)
42 | listeners.foreach(_())
43 | listeners = Set.empty
44 | }
45 |
46 | override def onServiceDisconnected(componentName: ComponentName) =
47 | chatService = None
48 |
49 | /**
50 | * Calls l as soon as [[ChatService]] first becomes available.
51 | */
52 | def runOnServiceConnected(l: () => Unit): Unit =
53 | chatService match {
54 | case Some(s) => l()
55 | case None => listeners += l
56 | }
57 |
58 | /**
59 | * Returns the [[ChatService]].
60 | *
61 | * Will only be set after [[runOnServiceConnected]].
62 | */
63 | def service = chatService.map(_.getConnectionHandler)
64 |
65 | def database = chatService.map(_.database)
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.activities
2 |
3 | import android.Manifest
4 | import android.bluetooth.BluetoothAdapter
5 | import android.content.pm.PackageManager
6 | import android.content.{Context, Intent}
7 | import android.os.Bundle
8 | import android.preference.PreferenceManager
9 | import android.support.v4.app.ActivityCompat
10 | import android.support.v4.content.ContextCompat
11 | import android.support.v7.app.AppCompatActivity
12 | import android.view.View.OnClickListener
13 | import android.view.inputmethod.{EditorInfo, InputMethodManager}
14 | import android.view.{KeyEvent, View}
15 | import android.widget.TextView.OnEditorActionListener
16 | import android.widget.{Button, EditText, TextView, Toast}
17 | import com.nutomic.ensichat.R
18 | import com.nutomic.ensichat.core.interfaces.SettingsInterface
19 | import com.nutomic.ensichat.core.interfaces.SettingsInterface._
20 |
21 | /**
22 | * Shown on first start, lets the user enter their name.
23 | */
24 | class FirstStartActivity extends AppCompatActivity with OnEditorActionListener with OnClickListener {
25 |
26 | private val KeyIsFirstStart = "first_start"
27 | private val RequestLocationPermission = 127
28 |
29 | private lazy val preferences = PreferenceManager.getDefaultSharedPreferences(this)
30 | private lazy val imm = getSystemService(Context.INPUT_METHOD_SERVICE)
31 | .asInstanceOf[InputMethodManager]
32 |
33 | private lazy val username = findViewById(R.id.username).asInstanceOf[EditText]
34 | private lazy val done = findViewById(R.id.done) .asInstanceOf[Button]
35 |
36 | override def onCreate(savedInstanceState: Bundle): Unit = {
37 | super.onCreate(savedInstanceState)
38 |
39 | setContentView(R.layout.activity_first_start)
40 | setTitle(R.string.welcome)
41 |
42 | val name = Option(BluetoothAdapter.getDefaultAdapter).map(_.getName.trim).getOrElse("")
43 | username.setText(name)
44 | username.setOnEditorActionListener(this)
45 | done.setOnClickListener(this)
46 |
47 | val permission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
48 | if (preferences.getBoolean(KeyIsFirstStart, true)) {
49 | imm.showSoftInput(username, InputMethodManager.SHOW_IMPLICIT)
50 | }
51 | else if (permission != PackageManager.PERMISSION_GRANTED) {
52 | requestLocationPermission()
53 | }
54 | else {
55 | startMainActivity()
56 | }
57 | }
58 |
59 | /**
60 | * Calls [[save]] on enter click.
61 | */
62 | override def onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean = {
63 | if (actionId == EditorInfo.IME_ACTION_DONE) {
64 | save()
65 | true
66 | }
67 | else
68 | false
69 | }
70 |
71 | override def onClick(v: View): Unit = save()
72 |
73 | /**
74 | * Saves username and default settings values, then calls [[startMainActivity]].
75 | */
76 | private def save(): Unit = {
77 | imm.hideSoftInputFromWindow(username.getWindowToken, 0)
78 |
79 | preferences
80 | .edit()
81 | .putBoolean(KeyIsFirstStart, false)
82 | .putString(KeyUserName, username.getText.toString.trim)
83 | .putString(KeyUserStatus, SettingsInterface.DefaultUserStatus)
84 | .putBoolean(KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
85 | .putString(KeyScanInterval, DefaultScanInterval.toString)
86 | .putString(KeyAddresses, DefaultAddresses)
87 | .apply()
88 |
89 | requestLocationPermission()
90 | }
91 |
92 | private def requestLocationPermission(): Unit =
93 | ActivityCompat.requestPermissions(this, Array(Manifest.permission.ACCESS_COARSE_LOCATION), RequestLocationPermission)
94 |
95 | override def onRequestPermissionsResult(requestCode: Int,
96 | permissions: Array[String], grantResults: Array[Int]): Unit = requestCode match {
97 | case RequestLocationPermission =>
98 | if (grantResults.length > 0 && grantResults(0) == PackageManager.PERMISSION_GRANTED) {
99 | startMainActivity()
100 | } else {
101 | Toast.makeText(this, R.string.toast_location_required, Toast.LENGTH_SHORT).show()
102 | }
103 | }
104 |
105 | def startMainActivity(): Unit = {
106 | val intent = new Intent(this, classOf[MainActivity])
107 | intent.setAction(MainActivity.ActionRequestBluetooth)
108 | startActivity(intent)
109 | finish()
110 | }
111 |
112 | }
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.activities
2 |
3 | import android.app.Activity
4 | import android.bluetooth.BluetoothAdapter
5 | import android.content._
6 | import android.os.Bundle
7 | import android.preference.PreferenceManager
8 | import android.view.MenuItem
9 | import android.widget.Toast
10 | import com.nutomic.ensichat.R
11 | import com.nutomic.ensichat.core.routing.Address
12 | import com.nutomic.ensichat.fragments.{ChatFragment, ContactsFragment}
13 |
14 | object MainActivity {
15 |
16 | /**
17 | * If this action is set, a dialog will be shown to request the device to be discoverable.
18 | *
19 | * This should only be used when the app is started from a launcher
20 | * (eg from [[FirstStartActivity]]).
21 | */
22 | val ActionRequestBluetooth = "request_bluetooth"
23 |
24 | val ActionOpenChat = "open_chat"
25 |
26 | val ExtraAddress = "address"
27 |
28 | val PrefWasBluetoothEnabled = "was_bluetooth_enabled"
29 |
30 | }
31 |
32 | /**
33 | * Main activity, entry point for app start.
34 | */
35 | class MainActivity extends EnsichatActivity {
36 |
37 | private val RequestSetDiscoverable = 1
38 |
39 | private var contactsFragment: ContactsFragment = _
40 |
41 | private var currentChat: Option[Address] = None
42 |
43 | /**
44 | * Initializes layout, starts service and requests Bluetooth to be discoverable.
45 | */
46 | override def onCreate(savedInstanceState: Bundle): Unit = {
47 | super.onCreate(savedInstanceState)
48 | setContentView(R.layout.activity_main)
49 |
50 | if (getIntent.getAction == MainActivity.ActionRequestBluetooth &&
51 | Option(BluetoothAdapter.getDefaultAdapter).isDefined) {
52 | val btAdapter = BluetoothAdapter.getDefaultAdapter
53 | PreferenceManager.getDefaultSharedPreferences(this)
54 | .edit()
55 | .putBoolean(MainActivity.PrefWasBluetoothEnabled, btAdapter.isEnabled)
56 | .apply()
57 |
58 | val intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
59 | intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
60 | startActivityForResult(intent, RequestSetDiscoverable)
61 | // Make sure this code isn't executed after screen rotate etc.
62 | getIntent.setAction(null)
63 | }
64 |
65 | val fm = getFragmentManager
66 | if (savedInstanceState != null) {
67 | contactsFragment = fm.getFragment(savedInstanceState, classOf[ContactsFragment].getName)
68 | .asInstanceOf[ContactsFragment]
69 | if (savedInstanceState.containsKey("current_chat")) {
70 | currentChat = Option(new Address(savedInstanceState.getByteArray("current_chat")))
71 | openChat(currentChat.get)
72 | }
73 | } else {
74 | contactsFragment = new ContactsFragment()
75 | fm.beginTransaction()
76 | .add(android.R.id.content, contactsFragment)
77 | .commit()
78 | }
79 | }
80 |
81 | override def onNewIntent(intent: Intent): Unit = {
82 | if (intent.getAction == MainActivity.ActionOpenChat)
83 | openChat(new Address(intent.getStringExtra(MainActivity.ExtraAddress)))
84 | }
85 |
86 | /**
87 | * Saves all fragment state.
88 | */
89 | override def onSaveInstanceState(outState: Bundle): Unit = {
90 | super.onSaveInstanceState(outState)
91 | getFragmentManager.putFragment(outState, classOf[ContactsFragment].getName, contactsFragment)
92 | currentChat.collect{case c => outState.putByteArray("current_chat", c.bytes)}
93 | }
94 |
95 | /**
96 | * Exits with error if bluetooth was not enabled/not set discoverable,
97 | */
98 | override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit =
99 | requestCode match {
100 | case RequestSetDiscoverable =>
101 | if (resultCode == Activity.RESULT_CANCELED) {
102 | Toast.makeText(this, R.string.toast_bluetooth_denied, Toast.LENGTH_LONG).show()
103 | }
104 | }
105 |
106 | /**
107 | * Opens a chat fragment for the given device, creating the fragment if needed.
108 | */
109 | def openChat(address: Address): Unit = {
110 | currentChat = Option(address)
111 | getFragmentManager
112 | .beginTransaction()
113 | .detach(contactsFragment)
114 | .add(android.R.id.content, new ChatFragment(address))
115 | .commit()
116 | Option(getSupportActionBar).foreach(_.setDisplayHomeAsUpEnabled(true))
117 | }
118 |
119 | /**
120 | * If in a ChatFragment, goes back up to contactsFragment.
121 | */
122 | override def onBackPressed(): Unit = {
123 | if (currentChat.isDefined) {
124 | getFragmentManager
125 | .beginTransaction()
126 | .remove(getFragmentManager.findFragmentById(android.R.id.content))
127 | .attach(contactsFragment)
128 | .commit()
129 | currentChat = None
130 | getSupportActionBar.setDisplayHomeAsUpEnabled(false)
131 | setTitle(R.string.app_name)
132 | } else
133 | super.onBackPressed()
134 | }
135 |
136 | override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
137 | case android.R.id.home =>
138 | if (currentChat.isDefined)
139 | onBackPressed()
140 | true;
141 | case _ =>
142 | super.onOptionsItemSelected(item);
143 | }
144 |
145 | }
146 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/activities/SettingsActivity.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.activities
2 |
3 | import android.app.Fragment
4 | import android.os.Bundle
5 | import android.support.v4.app.NavUtils
6 | import android.view.MenuItem
7 | import com.nutomic.ensichat.fragments.SettingsFragment
8 |
9 | /**
10 | * Holder for [[SettingsFragment]].
11 | */
12 | class SettingsActivity extends EnsichatActivity {
13 |
14 | private var fragment: Fragment = _
15 |
16 | override def onCreate(savedInstanceState: Bundle): Unit = {
17 | super.onCreate(savedInstanceState)
18 | getSupportActionBar.setDisplayHomeAsUpEnabled(true)
19 |
20 | val fm = getFragmentManager
21 | fragment =
22 | if (savedInstanceState != null) {
23 | fm.getFragment(savedInstanceState, "settings_fragment")
24 | } else {
25 | new SettingsFragment()
26 | }
27 | fm.beginTransaction()
28 | .replace(android.R.id.content, fragment)
29 | .commit()
30 | }
31 |
32 | override def onSaveInstanceState(outState: Bundle): Unit = {
33 | super.onSaveInstanceState(outState)
34 |
35 | getFragmentManager.putFragment(outState, "settings_fragment", fragment)
36 | }
37 |
38 | override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
39 | case android.R.id.home =>
40 | NavUtils.navigateUpFromSameTask(this)
41 | true
42 | case _ =>
43 | super.onOptionsItemSelected(item);
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.bluetooth
2 |
3 | import java.io.IOException
4 |
5 | import android.bluetooth.BluetoothSocket
6 | import android.util.Log
7 |
8 | /**
9 | * Attempts to connect to another device and calls [[onConnected]] on success.
10 | */
11 | class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
12 |
13 | private val Tag = "ConnectThread"
14 |
15 | private val socket = try {
16 | Option(device.btDevice.get.createInsecureRfcommSocketToServiceRecord(BluetoothInterface.AppUuid))
17 | } catch {
18 | case e: IOException =>
19 | Log.w(Tag, "Failed to open Bluetooth connection", e)
20 | None
21 | }
22 |
23 | override def run(): Unit = {
24 | if (socket.isEmpty)
25 | return
26 |
27 | Log.i(Tag, "Connecting to " + device.toString)
28 | try {
29 | socket.get.connect()
30 | } catch {
31 | case e: IOException =>
32 | Log.v(Tag, "Failed to connect to " + device.toString, e)
33 | try {
34 | socket.get.close()
35 | } catch {
36 | case e2: IOException =>
37 | Log.e(Tag, "Failed to close socket", e2)
38 | }
39 | return
40 | }
41 |
42 | Log.i(Tag, "Successfully connected to device " + device.name)
43 | onConnected(new Device(device.btDevice.get, true), socket.get)
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothDevice.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.bluetooth
2 |
3 | import android.bluetooth.BluetoothDevice
4 |
5 | private[bluetooth] object Device {
6 |
7 | /**
8 | * Holds bluetooth device IDs, which are just wrapped addresses (used for type safety).
9 | *
10 | * @param id A bluetooth device address.
11 | */
12 | case class ID(private val id: String) {
13 |
14 | require(id.matches("([A-Z0-9][A-Z0-9]:){5}[A-Z0-9][A-Z0-9]"), "Invalid device ID format")
15 |
16 | override def toString = id
17 |
18 | }
19 |
20 | }
21 |
22 | /**
23 | * Holds information about a remote bluetooth device.
24 | */
25 | private[bluetooth] case class Device(id: Device.ID, name: String, connected: Boolean,
26 | btDevice: Option[BluetoothDevice] = None) {
27 |
28 | def this(btDevice: BluetoothDevice, connected: Boolean) = {
29 | this(new Device.ID(btDevice.getAddress), btDevice.getName, connected, Option(btDevice))
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.bluetooth
2 |
3 | import java.io.IOException
4 |
5 | import android.bluetooth.{BluetoothAdapter, BluetoothSocket}
6 | import android.util.Log
7 |
8 | /**
9 | * Listens for incoming connections from other devices.
10 | *
11 | * @param name Service name to broadcast.
12 | */
13 | class BluetoothListenThread(name: String, adapter: BluetoothAdapter,
14 | onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
15 |
16 | private val Tag = "ListenThread"
17 |
18 | private val serverSocket =
19 | try {
20 | adapter.listenUsingInsecureRfcommWithServiceRecord(name, BluetoothInterface.AppUuid)
21 | } catch {
22 | case e: IOException =>
23 | Log.e(Tag, "Failed to create listener", e)
24 | null
25 | }
26 |
27 | override def run(): Unit = {
28 | var socket: BluetoothSocket = null
29 |
30 | while (true) {
31 | try {
32 | // This is a blocking call and will only return on a
33 | // successful connection or an exception
34 | socket = serverSocket.accept()
35 | } catch {
36 | case e: IOException =>
37 | // This happens if Bluetooth is disabled manually.
38 | Log.w(Tag, "Failed to accept new connection", e)
39 | return
40 | }
41 |
42 | val device: Device = new Device(socket.getRemoteDevice, true)
43 | Log.i(Tag, "Incoming connection from " + device.toString)
44 | onConnected(device, socket)
45 | }
46 | }
47 |
48 | def cancel(): Unit = {
49 | Log.i(Tag, "Canceling listening")
50 | try {
51 | serverSocket.close()
52 | } catch {
53 | case e: IOException =>
54 | Log.e(Tag, "Failed to close listener", e)
55 | }
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.bluetooth
2 |
3 | import java.io._
4 |
5 | import android.bluetooth.{BluetoothDevice, BluetoothSocket}
6 | import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
7 | import android.util.Log
8 | import com.nutomic.ensichat.core.messages.Message
9 | import Message.ReadMessageException
10 | import com.nutomic.ensichat.core.messages.Message
11 | import com.nutomic.ensichat.core.messages.body.ConnectionInfo
12 | import com.nutomic.ensichat.core.messages.header.MessageHeader
13 | import com.nutomic.ensichat.core.routing.Address
14 | import com.nutomic.ensichat.core.util.Crypto
15 | import org.joda.time.DateTime
16 |
17 | /**
18 | * Transfers data between connnected devices.
19 | *
20 | * @param device The bluetooth device to interact with.
21 | * @param socket An open socket to the given device.
22 | * @param onReceive Called when a message was received from the other device.
23 | */
24 | class BluetoothTransferThread(context: Context, device: Device, socket: BluetoothSocket,
25 | handler: BluetoothInterface, crypto: Crypto,
26 | onReceive: (Message, Device.ID) => Unit) extends Thread {
27 |
28 | private val connectionOpened = DateTime.now
29 |
30 | private val Tag = "TransferThread"
31 |
32 | private var isClosed = false
33 |
34 | private val inStream: InputStream =
35 | try {
36 | socket.getInputStream
37 | } catch {
38 | case e: IOException =>
39 | Log.e(Tag, "Failed to open stream", e)
40 | close()
41 | null
42 | }
43 |
44 | private val outStream: OutputStream =
45 | try {
46 | socket.getOutputStream
47 | } catch {
48 | case e: IOException =>
49 | Log.e(Tag, "Failed to open stream", e)
50 | close()
51 | null
52 | }
53 |
54 | private val disconnectReceiver = new BroadcastReceiver {
55 | override def onReceive(context: Context, intent: Intent): Unit = {
56 | val address = intent.getParcelableExtra[BluetoothDevice](BluetoothDevice.EXTRA_DEVICE).getAddress
57 | if (device.btDevice.get.getAddress == address) {
58 | Log.i(Tag, "Device with address " + address + " disconnected")
59 | close()
60 | }
61 | }
62 | }
63 |
64 | override def run(): Unit = {
65 | Log.i(Tag, "Starting data transfer with " + device.toString)
66 |
67 | context.registerReceiver(disconnectReceiver,
68 | new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED))
69 |
70 | send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
71 | Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
72 |
73 | while (socket.isConnected) {
74 | try {
75 | val msg = Message.read(inStream)
76 | Log.v(Tag, "Received " + msg)
77 |
78 | onReceive(msg, device.id)
79 | } catch {
80 | case e @ (_: ReadMessageException | _: IOException) =>
81 | Log.w(Tag, "Failed to read incoming message", e)
82 | close()
83 | return
84 | }
85 | }
86 | close()
87 | }
88 |
89 | def send(msg: Message): Unit = {
90 | try {
91 | outStream.write(msg.write)
92 | Log.v(Tag, "Sending " + msg)
93 | } catch {
94 | case e: IOException => Log.e(Tag, "Failed to write message", e)
95 | }
96 | }
97 |
98 | def close(): Unit = {
99 | if (isClosed)
100 | return
101 |
102 | isClosed = true
103 | context.unregisterReceiver(disconnectReceiver)
104 | try {
105 | Log.i(Tag, "Closing connection to " + device)
106 | inStream.close()
107 | outStream.close()
108 | socket.close()
109 | } catch {
110 | case e: IOException => Log.e(Tag, "Failed to close socket", e);
111 | } finally {
112 | handler.onConnectionClosed(connectionOpened, device.id)
113 | }
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.fragments
2 |
3 | import android.app.ListFragment
4 | import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
5 | import android.os.Bundle
6 | import android.support.v4.content.LocalBroadcastManager
7 | import android.support.v7.widget.Toolbar
8 | import android.view.View.OnClickListener
9 | import android.view.inputmethod.EditorInfo
10 | import android.view.{KeyEvent, LayoutInflater, View, ViewGroup}
11 | import android.widget.TextView.OnEditorActionListener
12 | import android.widget._
13 | import com.nutomic.ensichat.R
14 | import com.nutomic.ensichat.activities.EnsichatActivity
15 | import com.nutomic.ensichat.core.messages.body.Text
16 | import com.nutomic.ensichat.core.messages.Message
17 | import com.nutomic.ensichat.core.routing.Address
18 | import com.nutomic.ensichat.core.ConnectionHandler
19 | import com.nutomic.ensichat.service.CallbackHandler
20 | import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter}
21 |
22 | /**
23 | * Represents a single chat with another specific device.
24 | */
25 | class ChatFragment extends ListFragment with OnClickListener {
26 |
27 | /**
28 | * Fragments need to have a default constructor, so this is optional.
29 | */
30 | def this(address: Address) {
31 | this
32 | this.address = address
33 | }
34 |
35 | private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
36 |
37 | private var address: Address = _
38 |
39 | private var chatService: ConnectionHandler = _
40 |
41 | private var sendButton: Button = _
42 |
43 | private var messageText: EditText = _
44 |
45 | private var listView: ListView = _
46 |
47 | private var adapter: DatesAdapter = _
48 |
49 | override def onActivityCreated(savedInstanceState: Bundle): Unit = {
50 | super.onActivityCreated(savedInstanceState)
51 |
52 | activity.runOnServiceConnected(() => {
53 | chatService = activity.service.get
54 |
55 | activity.database.get.getContact(address).foreach(c => getActivity.setTitle(c.name))
56 |
57 | adapter = new DatesAdapter(getActivity,
58 | new MessagesAdapter(getActivity, activity.database.get.getMessages(address), address))
59 |
60 | if (listView != null) {
61 | listView.setAdapter(adapter)
62 | }
63 | })
64 | }
65 |
66 | override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
67 | savedInstanceState: Bundle): View = {
68 | val view = inflater.inflate(R.layout.fragment_chat, container, false)
69 | val toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
70 | activity.setSupportActionBar(toolbar)
71 | activity.getSupportActionBar.setDisplayHomeAsUpEnabled(true)
72 | sendButton = view.findViewById(R.id.send).asInstanceOf[Button]
73 | sendButton.setOnClickListener(this)
74 | messageText = view.findViewById(R.id.message).asInstanceOf[EditText]
75 | messageText.setOnEditorActionListener(new OnEditorActionListener {
76 | override def onEditorAction(view: TextView, actionId: Int, event: KeyEvent): Boolean = {
77 | if (actionId == EditorInfo.IME_ACTION_DONE) {
78 | onClick(sendButton)
79 | true
80 | } else
81 | false
82 | }
83 | })
84 | listView = view.findViewById(android.R.id.list).asInstanceOf[ListView]
85 | listView.setAdapter(adapter)
86 | view
87 | }
88 |
89 | override def onCreate(savedInstanceState: Bundle): Unit = {
90 | super.onCreate(savedInstanceState)
91 |
92 | if (savedInstanceState != null)
93 | address = new Address(savedInstanceState.getByteArray("address"))
94 |
95 | LocalBroadcastManager.getInstance(getActivity)
96 | .registerReceiver(onMessageReceivedReceiver, new IntentFilter(CallbackHandler.ActionMessageReceived))
97 | }
98 |
99 | override def onSaveInstanceState(outState: Bundle): Unit = {
100 | super.onSaveInstanceState(outState)
101 | outState.putByteArray("address", address.bytes)
102 | }
103 |
104 | override def onDestroy(): Unit = {
105 | super.onDestroy()
106 | LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(onMessageReceivedReceiver)
107 | }
108 |
109 | /**
110 | * Send message if send button was clicked.
111 | */
112 | override def onClick(view: View): Unit = view.getId match {
113 | case R.id.send =>
114 | val text = messageText.getText.toString.trim
115 | if (!text.isEmpty) {
116 | val message = new Text(text.toString)
117 | chatService.sendTo(address, message)
118 | messageText.getText.clear()
119 | }
120 | }
121 |
122 | /**
123 | * Displays new messages in UI.
124 | */
125 | private val onMessageReceivedReceiver = new BroadcastReceiver {
126 | override def onReceive(context: Context, intent: Intent): Unit = {
127 | val msg = intent.getSerializableExtra(CallbackHandler.ExtraMessage).asInstanceOf[Message]
128 | if (!Set(msg.header.origin, msg.header.target).contains(address))
129 | return
130 |
131 | msg.body match {
132 | case _: Text =>
133 | val messages = activity.database.get.getMessages(address)
134 | adapter.replaceItems(messages)
135 | case _ =>
136 | }
137 | }
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.fragments
2 |
3 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener
4 | import android.content.{Intent, SharedPreferences}
5 | import android.os.Bundle
6 | import android.preference.{PreferenceFragment, PreferenceManager}
7 | import com.nutomic.ensichat.R
8 | import com.nutomic.ensichat.activities.EnsichatActivity
9 | import com.nutomic.ensichat.core.interfaces.SettingsInterface._
10 | import com.nutomic.ensichat.core.messages.body.UserInfo
11 | import com.nutomic.ensichat.fragments.SettingsFragment._
12 | import com.nutomic.ensichat.service.ChatService
13 |
14 | object SettingsFragment {
15 | val Version = "version"
16 | }
17 |
18 | /**
19 | * Settings screen.
20 | */
21 | class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
22 |
23 | private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
24 |
25 | private lazy val version = findPreference(Version)
26 |
27 | private lazy val prefs = PreferenceManager.getDefaultSharedPreferences(getActivity)
28 |
29 | override def onCreate(savedInstanceState: Bundle): Unit = {
30 | super.onCreate(savedInstanceState)
31 |
32 | addPreferencesFromResource(R.xml.settings)
33 |
34 | val packageInfo = getActivity.getPackageManager.getPackageInfo(getActivity.getPackageName, 0)
35 | version.setSummary(packageInfo.versionName)
36 | prefs.registerOnSharedPreferenceChangeListener(this)
37 | }
38 |
39 | override def onDestroy(): Unit = {
40 | super.onDestroy()
41 | prefs.unregisterOnSharedPreferenceChangeListener(this)
42 | }
43 |
44 | /**
45 | * Sends the updated username or status to all contacts.
46 | */
47 | override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
48 | key match {
49 | case KeyUserName | KeyUserStatus =>
50 | val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, ""))
51 | activity.database.get.getContacts.foreach(c => activity.service.get.sendTo(c.address, ui))
52 | case KeyAddresses =>
53 | val intent = new Intent(getActivity, classOf[ChatService])
54 | intent.setAction(ChatService.ActionNetworkChanged)
55 | getActivity.startService(intent)
56 | case _ =>
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/fragments/UserInfoFragment.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.fragments
2 |
3 | import android.app.{AlertDialog, Dialog, DialogFragment}
4 | import android.content.{ClipData, ClipboardManager, Context}
5 | import android.graphics.{Bitmap, Color}
6 | import android.os.Bundle
7 | import android.view.View.{OnClickListener, OnLongClickListener}
8 | import android.view.{LayoutInflater, View}
9 | import android.widget.{ImageView, TextView, Toast}
10 | import com.google.zxing.BarcodeFormat
11 | import com.google.zxing.common.BitMatrix
12 | import com.google.zxing.qrcode.QRCodeWriter
13 | import com.nutomic.ensichat.R
14 | import com.nutomic.ensichat.core.routing.Address
15 | import com.nutomic.ensichat.util.IdenticonGenerator
16 |
17 | object UserInfoFragment {
18 | val ExtraAddress = "address"
19 | val ExtraUserName = "user_name"
20 | }
21 |
22 | /**
23 | * Displays identicon, username and address for a user.
24 | *
25 | * Use [[UserInfoFragment#getInstance]] to invoke.
26 | */
27 | class UserInfoFragment extends DialogFragment with OnLongClickListener {
28 |
29 | private lazy val address = new Address(getArguments.getString(UserInfoFragment.ExtraAddress))
30 | private lazy val userName = getArguments.getString(UserInfoFragment.ExtraUserName)
31 |
32 | override def onCreateDialog(savedInstanceState: Bundle): Dialog = {
33 | val view = LayoutInflater.from(getActivity).inflate(R.layout.fragment_identicon, null)
34 |
35 | view.findViewById(R.id.identicon)
36 | .asInstanceOf[ImageView]
37 | .setImageBitmap(IdenticonGenerator.generate(address, (150, 150), getActivity))
38 | val addressTextView = view.findViewById(R.id.address)
39 | .asInstanceOf[TextView]
40 | addressTextView.setText(getString(R.string.address_colon, address.toString()))
41 | addressTextView.setOnLongClickListener(this)
42 | addressTextView.setOnClickListener(new OnClickListener {
43 | override def onClick(v: View): Unit = onLongClick(v)
44 | })
45 |
46 | val matrix = new QRCodeWriter().encode(address.toString(), BarcodeFormat.QR_CODE, 150, 150)
47 | view.findViewById(R.id.qr_code)
48 | .asInstanceOf[ImageView]
49 | .setImageBitmap(renderMatrix(matrix))
50 |
51 | new AlertDialog.Builder(getActivity)
52 | .setTitle(userName)
53 | .setView(view)
54 | .setPositiveButton(android.R.string.ok, null)
55 | .create()
56 | }
57 |
58 | override def onLongClick(v: View): Boolean = {
59 | val cm = getContext.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
60 | val clip = ClipData.newPlainText(getContext.getString(R.string.ensichat_user_address), address.toString)
61 | cm.setPrimaryClip(clip)
62 | Toast.makeText(getContext, R.string.address_copied_to_clipboard, Toast.LENGTH_SHORT).show()
63 | true
64 | }
65 |
66 | /**
67 | * Converts a [[BitMatrix]] instance into a [[Bitmap]].
68 | */
69 | private def renderMatrix(bitMatrix: BitMatrix): Bitmap = {
70 | val height = bitMatrix.getHeight
71 | val width = bitMatrix.getWidth
72 | val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
73 | for (x <- 0 until width) {
74 | for (y <- 0 until height) {
75 | val color =
76 | if (bitMatrix.get(x,y))
77 | Color.BLACK
78 | else
79 | Color.WHITE
80 | bmp.setPixel(x, y, color)
81 | }
82 | }
83 | bmp
84 | }
85 |
86 | }
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/service/BootReceiver.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.service
2 |
3 | import android.content.{BroadcastReceiver, Context, Intent}
4 | import android.preference.PreferenceManager
5 |
6 | /**
7 | * Starts [[ChatService]] on boot if preference is enabled.
8 | */
9 | class BootReceiver extends BroadcastReceiver {
10 |
11 | override def onReceive(context: Context, intent: Intent): Unit = {
12 | val sp = PreferenceManager.getDefaultSharedPreferences(context)
13 | context.startService(new Intent(context, classOf[ChatService]))
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.service
2 |
3 | import android.content.Intent
4 | import android.support.v4.content.LocalBroadcastManager
5 | import com.nutomic.ensichat.core.interfaces.CallbackInterface
6 | import com.nutomic.ensichat.core.ConnectionHandler
7 | import com.nutomic.ensichat.core.messages.Message
8 | import com.nutomic.ensichat.service.CallbackHandler._
9 |
10 | object CallbackHandler {
11 |
12 | val ActionMessageReceived = "message_received"
13 | val ActionConnectionsChanged = "connections_changed"
14 | val ActionContactsUpdated = "contacts_updated"
15 |
16 | val ExtraMessage = "extra_message"
17 |
18 | }
19 |
20 | /**
21 | * Receives events from [[ConnectionHandler]] and sends them as local broadcasts.
22 | */
23 | class CallbackHandler(chatService: ChatService, notificationHandler: NotificationHandler)
24 | extends CallbackInterface {
25 |
26 | def onMessageReceived(msg: Message): Unit = {
27 | notificationHandler.onMessageReceived(msg)
28 | val i = new Intent(ActionMessageReceived)
29 | i.putExtra(ExtraMessage, msg)
30 | LocalBroadcastManager.getInstance(chatService)
31 | .sendBroadcast(i)
32 |
33 | }
34 |
35 | def onConnectionsChanged(): Unit = {
36 | val i = new Intent(ActionConnectionsChanged)
37 | LocalBroadcastManager.getInstance(chatService)
38 | .sendBroadcast(i)
39 | notificationHandler
40 | .updatePersistentNotification(chatService.getConnectionHandler.connections().size)
41 | }
42 |
43 | def onContactsUpdated(): Unit = {
44 | val i = new Intent(ActionContactsUpdated)
45 | LocalBroadcastManager.getInstance(chatService)
46 | .sendBroadcast(i)
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.service
2 |
3 | import java.io.File
4 |
5 | import android.app.Service
6 | import android.bluetooth.BluetoothAdapter
7 | import android.content.{Context, Intent, IntentFilter}
8 | import android.net.ConnectivityManager
9 | import android.os.Handler
10 | import com.nutomic.ensichat.bluetooth.BluetoothInterface
11 | import com.nutomic.ensichat.core.interfaces.TransmissionInterface
12 | import com.nutomic.ensichat.core.util.{Crypto, Database}
13 | import com.nutomic.ensichat.core.ConnectionHandler
14 | import com.nutomic.ensichat.util.{NetworkChangedReceiver, SettingsWrapper}
15 |
16 | object ChatService {
17 |
18 | case class Binder(service: ChatService) extends android.os.Binder
19 |
20 | private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
21 | def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
22 |
23 | val ActionNetworkChanged = "network_changed"
24 |
25 | }
26 |
27 | class ChatService extends Service {
28 |
29 | private lazy val binder = new ChatService.Binder(this)
30 |
31 | private lazy val notificationHandler = new NotificationHandler(this)
32 |
33 | private val callbackHandler = new CallbackHandler(this, notificationHandler)
34 |
35 | private def settingsWrapper = new SettingsWrapper(this)
36 |
37 | lazy val database = new Database(getDatabasePath("database"), settingsWrapper, callbackHandler)
38 |
39 | private lazy val connectionHandler =
40 | new ConnectionHandler(settingsWrapper, database, callbackHandler, ChatService.newCrypto(this))
41 |
42 | private val networkReceiver = new NetworkChangedReceiver()
43 |
44 | override def onBind(intent: Intent) = binder
45 |
46 | override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
47 | Option(intent).foreach { i =>
48 | if (i.getAction == ChatService.ActionNetworkChanged)
49 | connectionHandler.internetConnectionChanged()
50 | }
51 |
52 | Service.START_STICKY
53 | }
54 |
55 | /**
56 | * Generates keys and starts Bluetooth interface.
57 | */
58 | override def onCreate(): Unit = {
59 | super.onCreate()
60 | notificationHandler.updatePersistentNotification(getConnectionHandler.connections().size)
61 | var additionalInterfaces = Set[TransmissionInterface]()
62 | if (Option(BluetoothAdapter.getDefaultAdapter).isDefined)
63 | additionalInterfaces += new BluetoothInterface(this, new Handler(), connectionHandler)
64 |
65 | connectionHandler.start(additionalInterfaces)
66 | registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
67 | }
68 |
69 | override def onDestroy(): Unit = {
70 | notificationHandler.stopPersistentNotification()
71 | connectionHandler.stop()
72 | unregisterReceiver(networkReceiver)
73 | }
74 |
75 | def getConnectionHandler = connectionHandler
76 |
77 | }
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.service
2 |
3 | import android.app.{Notification, NotificationManager, PendingIntent}
4 | import android.content.{Context, Intent}
5 | import android.preference.PreferenceManager
6 | import android.support.v4.app.NotificationCompat
7 | import com.nutomic.ensichat.R
8 | import com.nutomic.ensichat.activities.MainActivity
9 | import com.nutomic.ensichat.core.messages.body.Text
10 | import com.nutomic.ensichat.core.interfaces.SettingsInterface
11 | import com.nutomic.ensichat.core.messages.Message
12 | import com.nutomic.ensichat.service.NotificationHandler._
13 |
14 | object NotificationHandler {
15 |
16 | private val NotificationIdRunning = 1
17 |
18 | private val NotificationIdNewMessage = 2
19 |
20 | }
21 |
22 | /**
23 | * Displays notifications for new messages and while the app is running.
24 | */
25 | class NotificationHandler(context: Context) {
26 |
27 | private lazy val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
28 | .asInstanceOf[NotificationManager]
29 |
30 | private var persistentNotificationShutdown = false
31 |
32 | def updatePersistentNotification(connections: Int): Unit = {
33 | if (persistentNotificationShutdown)
34 | return
35 |
36 | val intent = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
37 | val info = context.getResources
38 | .getQuantityString(R.plurals.notification_connections, connections, connections.toString)
39 |
40 | val notification = new NotificationCompat.Builder(context)
41 | .setSmallIcon(R.drawable.ic_launcher)
42 | .setContentTitle(context.getString(R.string.app_name))
43 | .setContentText(info)
44 | .setContentIntent(intent)
45 | .setOngoing(true)
46 | .setPriority(Notification.PRIORITY_MIN)
47 | .build()
48 | notificationManager.notify(NotificationIdRunning, notification)
49 | }
50 |
51 | /**
52 | * Cancels the persistent notification.
53 | *
54 | * After calling this method, [[updatePersistentNotification()]] will have no effect.
55 | */
56 | def stopPersistentNotification() = {
57 | persistentNotificationShutdown = true
58 | notificationManager.cancel(NotificationIdRunning)
59 | }
60 |
61 | def onMessageReceived(msg: Message): Unit = msg.body match {
62 | case text: Text =>
63 | if (msg.header.origin == ChatService.newCrypto(context).localAddress)
64 | return
65 |
66 | val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
67 | val notification = new NotificationCompat.Builder(context)
68 | .setSmallIcon(R.drawable.ic_launcher)
69 | .setContentTitle(context.getString(R.string.notification_message))
70 | .setContentText(text.text)
71 | .setDefaults(defaults())
72 | .setContentIntent(pi)
73 | .setAutoCancel(true)
74 | .build()
75 |
76 | notificationManager.notify(NotificationIdNewMessage, notification)
77 | case _ =>
78 | }
79 |
80 | /**
81 | * Returns the default notification options that should be used.
82 | */
83 | private def defaults(): Int = {
84 | val sp = PreferenceManager.getDefaultSharedPreferences(context)
85 | if (sp.getBoolean(SettingsInterface.KeyNotificationSoundsOn, SettingsInterface.DefaultNotificationSoundsOn))
86 | Notification.DEFAULT_ALL
87 | else
88 | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.util
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap.Config
5 | import android.graphics.{Bitmap, Canvas, Color}
6 | import com.nutomic.ensichat.core.routing.Address
7 |
8 | /**
9 | * Calculates a unique identicon for the given hash.
10 | *
11 | * Based on "Contact Identicons" by David Hamp-Gonsalves (converted from Java to Scala).
12 | * https://github.com/davidhampgonsalves/Contact-Identicons
13 | */
14 | object IdenticonGenerator {
15 |
16 | private val Height: Int = 5
17 |
18 | private val Width: Int = 5
19 |
20 | /**
21 | * Generates an identicon for the key.
22 | *
23 | * The identicon size is fixed to [[Width]]x[[Height]].
24 | *
25 | * @param size The size of the bitmap returned in pixels (widthxheight).
26 | */
27 | def generate(address: Address, size: (Int, Int), context: Context): Bitmap = {
28 | val hash = address.bytes
29 |
30 | // Create base image and colors.
31 | var identicon = Bitmap.createBitmap(Width, Height, Config.ARGB_8888)
32 | val background = Color.parseColor("#f0f0f0")
33 | val r = hash(0) & 255
34 | val g = hash(1) & 255
35 | val b = hash(2) & 255
36 | val foreground = Color.argb(255, r, g, b)
37 |
38 | // Color pixels.
39 | for (x <- 0 until Width) {
40 | val i = if (x < 3) x else 4 - x
41 | var pixelColor: Int = 0
42 | for (y <- 0 until Height) {
43 | pixelColor = if ((hash(i) >> y & 1) == 1) foreground else background
44 | identicon.setPixel(x, y, pixelColor)
45 | }
46 | }
47 |
48 | // Add border.
49 | val bmpWithBorder = Bitmap.createBitmap(12, 12, identicon.getConfig)
50 | val canvas = new Canvas(bmpWithBorder)
51 | canvas.drawColor(background)
52 | identicon = Bitmap.createScaledBitmap(identicon, 10, 10, false)
53 | canvas.drawBitmap(identicon, 1, 1, null)
54 |
55 | Bitmap.createScaledBitmap(identicon, size._1, size._2, false)
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.util
2 |
3 | import android.content.{BroadcastReceiver, Context, Intent}
4 | import android.net.ConnectivityManager
5 | import com.nutomic.ensichat.service.ChatService
6 |
7 | /**
8 | * Forwards network changed intents to [[ChatService]].
9 | *
10 | * HACK: Because [[ConnectivityManager.CONNECTIVITY_ACTION]] is a sticky intent, and we register it
11 | * from Scala, an intent is sent as soon as the receiver is registered. As a workaround, we
12 | * ignore the first intent received.
13 | * Alternatively, we can register the receiver in the manifest, but that will start the
14 | * service (so it only works if the service runs permanently, with no exit).
15 | */
16 | class NetworkChangedReceiver extends BroadcastReceiver {
17 |
18 | private var isFirstIntent = true
19 |
20 | override def onReceive(context: Context, intent: Intent): Unit = {
21 | if (isFirstIntent) {
22 | isFirstIntent = false
23 | return
24 | }
25 |
26 | val intent = new Intent(context, classOf[ChatService])
27 | intent.setAction(ChatService.ActionNetworkChanged)
28 | context.startService(intent)
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.util
2 |
3 | import android.content.Context
4 | import android.preference.PreferenceManager
5 | import com.nutomic.ensichat.core.interfaces.SettingsInterface
6 |
7 | class SettingsWrapper(context: Context) extends SettingsInterface {
8 |
9 | private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
10 |
11 | override def get[T](key: String, default: T): T = default match {
12 | case s: String => prefs.getString(key, s).asInstanceOf[T]
13 | case i: Int => prefs.getInt(key, i).asInstanceOf[T]
14 | case l: Long => prefs.getLong(key, l).asInstanceOf[T]
15 | }
16 |
17 | override def put[T](key: String, value: T): Unit = value match {
18 | case s: String => prefs.edit().putString(key, s).apply()
19 | case i: Int => prefs.edit().putInt(key, i).apply()
20 | case l: Long => prefs.edit().putLong(key, l).apply()
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/views/DatesAdapter.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.views
2 |
3 | import java.text.DateFormat
4 |
5 | import android.content.Context
6 | import com.mobsandgeeks.adapters.{Sectionizer, SimpleSectionAdapter}
7 | import com.nutomic.ensichat.R
8 | import com.nutomic.ensichat.core.messages.Message
9 |
10 | import scala.collection.JavaConverters._
11 |
12 | object DatesAdapter {
13 |
14 | private val Sectionizer = new Sectionizer[Message]() {
15 | override def getSectionTitleForItem(item: Message): String = {
16 | DateFormat
17 | .getDateInstance(DateFormat.MEDIUM)
18 | .format(item.header.time.get.toDate)
19 | }
20 | }
21 |
22 | }
23 |
24 | /**
25 | * Wraps [[MessagesAdapter]] and shows date between messages.
26 | */
27 | class DatesAdapter(context: Context, messagesAdapter: MessagesAdapter)
28 | extends SimpleSectionAdapter[Message](context, messagesAdapter, R.layout.item_date, R.id.date,
29 | DatesAdapter.Sectionizer) {
30 |
31 | def replaceItems(items: Seq[Message]): Unit = {
32 | messagesAdapter.clear()
33 | messagesAdapter.addAll(items.asJava)
34 | notifyDataSetChanged()
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/views/MessagesAdapter.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.views
2 |
3 | import java.text.DateFormat
4 | import java.util
5 |
6 | import android.content.Context
7 | import android.view._
8 | import android.widget._
9 | import com.mobsandgeeks.adapters.{InstantAdapter, SimpleSectionAdapter, ViewHandler}
10 | import com.nutomic.ensichat.R
11 | import com.nutomic.ensichat.core.messages.body.Text
12 | import com.nutomic.ensichat.core.messages.Message
13 | import com.nutomic.ensichat.core.routing.Address
14 | import com.nutomic.ensichat.views.MessagesAdapter._
15 |
16 | object MessagesAdapter {
17 |
18 | private def itemsAsMutableList(items: Seq[Message]): util.List[Message] = {
19 | val list = new util.ArrayList[Message]()
20 | items.foreach(list.add)
21 | list
22 | }
23 |
24 | }
25 |
26 | /**
27 | * Displays [[Message]]s in ListView.
28 | *
29 | * We just use the instant adapter for compatibility with [[SimpleSectionAdapter]], but don't use
30 | * the annotations (as it breaks separation of presentation and content).
31 | */
32 | class MessagesAdapter(context: Context, items: Seq[Message], remoteAddress: Address) extends
33 | InstantAdapter[Message](context, R.layout.item_message, classOf[Message],
34 | itemsAsMutableList(items)) {
35 |
36 | private val MessagePaddingLarge = 50
37 | private val MessagePaddingSmall = 10
38 |
39 | setViewHandler(R.id.root, new ViewHandler[Message] {
40 | override def handleView(adapter: ListAdapter, parent: View, view: View, msg: Message,
41 | position: Int): Unit = {
42 | val root = view.asInstanceOf[LinearLayout]
43 | val container = view.findViewById(R.id.container).asInstanceOf[LinearLayout]
44 | val text = view.findViewById(R.id.text).asInstanceOf[TextView]
45 | val time = view.findViewById(R.id.time).asInstanceOf[TextView]
46 |
47 | text.setText(msg.body.asInstanceOf[Text].text)
48 | val formattedDate = DateFormat
49 | .getTimeInstance(DateFormat.SHORT)
50 | .format(msg.header.time.get.toDate)
51 | time.setText(formattedDate)
52 |
53 | val paddingLarge = (MessagePaddingLarge * context.getResources.getDisplayMetrics.density).toInt
54 | val paddingSmall = (MessagePaddingSmall * context.getResources.getDisplayMetrics.density).toInt
55 | if (msg.header.origin != remoteAddress) {
56 | container.setGravity(Gravity.RIGHT)
57 | root.setGravity(Gravity.RIGHT)
58 | root.setPadding(paddingLarge, 0, paddingSmall, 0)
59 | } else {
60 | container.setGravity(Gravity.LEFT)
61 | root.setGravity(Gravity.LEFT)
62 | root.setPadding(paddingSmall, 0, paddingLarge, 0)
63 | }
64 | }
65 | })
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/android/src/main/scala/com/nutomic/ensichat/views/UsersAdapter.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.views
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.view.View.OnClickListener
7 | import android.view.{LayoutInflater, View, ViewGroup}
8 | import android.widget.{ArrayAdapter, ImageView, TextView}
9 | import com.nutomic.ensichat.R
10 | import com.nutomic.ensichat.core.util.User
11 | import com.nutomic.ensichat.fragments.UserInfoFragment
12 | import com.nutomic.ensichat.util.IdenticonGenerator
13 |
14 | /**
15 | * Displays [[User]]s in ListView.
16 | */
17 | class UsersAdapter(activity: Activity) extends ArrayAdapter[User](activity, 0) with OnClickListener {
18 |
19 | override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
20 | val view =
21 | if (convertView == null) {
22 | activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
23 | .asInstanceOf[LayoutInflater]
24 | .inflate(R.layout.item_user, parent, false)
25 | } else
26 | convertView
27 |
28 | val identicon = view.findViewById(R.id.identicon).asInstanceOf[ImageView]
29 | val title = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
30 | val summary = view.findViewById(android.R.id.text2).asInstanceOf[TextView]
31 |
32 | val user = getItem(position)
33 | identicon.setImageBitmap(IdenticonGenerator.generate(user.address, (50, 50), activity))
34 | identicon.setOnClickListener(this)
35 | identicon.setTag(user)
36 | title.setText(user.name)
37 | summary.setText(user.status)
38 | view
39 | }
40 |
41 | override def onClick(v: View): Unit = {
42 | val user = v.getTag.asInstanceOf[User]
43 | val fragment = new UserInfoFragment()
44 | val bundle = new Bundle()
45 | bundle.putString(UserInfoFragment.ExtraAddress, user.address.toString)
46 | bundle.putString(UserInfoFragment.ExtraUserName, user.name)
47 | fragment.setArguments(bundle)
48 | fragment.show(activity.getFragmentManager, "dialog")
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.ben-manes.versions'
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.2.0'
9 | classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
10 | }
11 | }
12 |
13 | allprojects {
14 | repositories {
15 | jcenter()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'scala'
2 |
3 | dependencies {
4 | compile 'org.scala-lang:scala-library:2.11.7'
5 | compile 'com.h2database:h2:1.4.192'
6 | compile 'com.typesafe.slick:slick_2.11:3.2.0-M1'
7 | compile 'com.typesafe.scala-logging:scala-logging_2.11:3.5.0'
8 | compile 'joda-time:joda-time:2.9.4'
9 | testCompile 'junit:junit:4.12'
10 | }
11 |
12 | test {
13 | systemProperty "testDir", new File(buildDir, "/test/").toString()
14 | }
15 |
--------------------------------------------------------------------------------
/core/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | System.out
5 |
6 | %d{HH:mm:ss} %level/%logger{0}: %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.interfaces
2 |
3 | import com.nutomic.ensichat.core.messages.Message
4 |
5 | trait CallbackInterface {
6 |
7 | def onMessageReceived(msg: Message): Unit
8 |
9 | def onConnectionsChanged(): Unit
10 |
11 | def onContactsUpdated(): Unit
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.interfaces
2 |
3 | object SettingsInterface {
4 |
5 | val KeyUserName = "user_name"
6 | val KeyUserStatus = "user_status"
7 | val KeyNotificationSoundsOn = "notification_sounds"
8 |
9 | /**
10 | * NOTE: Stored as string.
11 | */
12 | val KeyScanInterval = "scan_interval_seconds"
13 |
14 | /**
15 | * NOTE: Stored as comma separated string.
16 | */
17 | val KeyAddresses = "servers"
18 |
19 | val DefaultUserStatus = "Let's chat!"
20 | val DefaultScanInterval = 15
21 | val DefaultNotificationSoundsOn = true
22 | // When updating this, be sure to adjust the code in [[InternetInterface.create]].
23 | val DefaultAddresses = Set("ensichat.nutomic.com:26344", "trinity.nutomic.com:26344").mkString(", ")
24 |
25 | }
26 |
27 | /**
28 | * Interface for persistent storage of key value pairs.
29 | *
30 | * Must support at least storage of String, Int, Long.
31 | */
32 | trait SettingsInterface {
33 |
34 | def put[T](key: String, value: T): Unit
35 | def get[T](key: String, default: T): T
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/interfaces/TransmissionInterface.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.interfaces
2 |
3 | import com.nutomic.ensichat.core.messages.Message
4 | import com.nutomic.ensichat.core.routing.Address
5 |
6 | /**
7 | * Transfers data to another node over a certain medium (eg Internet or Bluetooth).
8 | */
9 | trait TransmissionInterface {
10 |
11 | def create(): Unit
12 |
13 | def destroy(): Unit
14 |
15 | def send(nextHop: Address, msg: Message): Unit
16 |
17 | def getConnections: Set[Address]
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.internet
2 |
3 | import java.io.{IOException, InputStream, OutputStream}
4 | import java.net.{InetAddress, Socket}
5 |
6 | import com.nutomic.ensichat.core.messages.Message
7 | import com.nutomic.ensichat.core.messages.Message.ReadMessageException
8 | import com.nutomic.ensichat.core.messages.body.ConnectionInfo
9 | import com.nutomic.ensichat.core.messages.header.MessageHeader
10 | import com.nutomic.ensichat.core.routing.Address
11 | import com.nutomic.ensichat.core.util.Crypto
12 | import com.typesafe.scalalogging.Logger
13 | import org.joda.time.DateTime
14 |
15 | /**
16 | * Encapsulates an active connection to another node.
17 | */
18 | private[core] class InternetConnectionThread(socket: Socket, crypto: Crypto,
19 | onDisconnected: (InternetConnectionThread) => Unit,
20 | onReceive: (Message, InternetConnectionThread) => Unit)
21 | extends Thread {
22 |
23 | val connectionOpened = DateTime.now
24 |
25 | private val logger = Logger(this.getClass)
26 |
27 | private val inStream: InputStream =
28 | try {
29 | socket.getInputStream
30 | } catch {
31 | case e: IOException =>
32 | logger.error("Failed to open stream", e)
33 | close()
34 | null
35 | }
36 |
37 | private val outStream: OutputStream =
38 | try {
39 | socket.getOutputStream
40 | } catch {
41 | case e: IOException =>
42 | logger.error("Failed to open stream", e)
43 | close()
44 | null
45 | }
46 |
47 | def internetAddress(): InetAddress = {
48 | socket.getInetAddress
49 | }
50 |
51 | override def run(): Unit = {
52 | logger.info("Connection opened to " + socket.getInetAddress)
53 |
54 | send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
55 | Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
56 |
57 | try {
58 | socket.setKeepAlive(true)
59 | while (socket.isConnected) {
60 | val msg = Message.read(inStream)
61 | logger.trace("Received " + msg)
62 |
63 | onReceive(msg, this)
64 | }
65 | } catch {
66 | case e @ (_: ReadMessageException | _: IOException) =>
67 | logger.warn("Failed to read incoming message", e)
68 | close()
69 | return
70 | }
71 | close()
72 | }
73 |
74 | def send(msg: Message): Unit = {
75 | try {
76 | outStream.write(msg.write)
77 | } catch {
78 | case e: IOException => logger.error("Failed to write message", e)
79 | }
80 | }
81 |
82 | def close(): Unit = {
83 | try {
84 | socket.close()
85 | } catch {
86 | case e: IOException => logger.warn("Failed to close socket", e)
87 | }
88 | onDisconnected(this)
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.internet
2 |
3 | import java.net.{InetAddress, Socket}
4 |
5 | import com.nutomic.ensichat.core.ConnectionHandler
6 | import com.nutomic.ensichat.core.interfaces.{SettingsInterface, TransmissionInterface}
7 | import com.nutomic.ensichat.core.messages.Message
8 | import com.nutomic.ensichat.core.messages.body.ConnectionInfo
9 | import com.nutomic.ensichat.core.routing.Address
10 | import com.nutomic.ensichat.core.util.{Crypto, FutureHelper}
11 | import com.typesafe.scalalogging.Logger
12 | import org.joda.time.{DateTime, Duration}
13 |
14 | import scala.concurrent.ExecutionContext.Implicits.global
15 | import scala.concurrent.Future
16 | import scala.util.Random
17 |
18 | private[core] object InternetInterface {
19 |
20 | val DefaultPort = 26344
21 |
22 | }
23 |
24 | /**
25 | * Handles all Internet connectivity.
26 | */
27 | private[core] class InternetInterface(connectionHandler: ConnectionHandler, crypto: Crypto,
28 | settings: SettingsInterface, port: Int)
29 | extends TransmissionInterface {
30 |
31 | private val logger = Logger(this.getClass)
32 |
33 | private lazy val serverThread =
34 | new InternetServerThread(crypto, port, onConnected, onDisconnected, onReceiveMessage)
35 |
36 | private var connections = Set[InternetConnectionThread]()
37 |
38 | private var addressDeviceMap = Map[Address, InternetConnectionThread]()
39 |
40 | /**
41 | * Initializes and starts discovery and listening.
42 | */
43 | override def create(): Unit = {
44 | val servers = settings.get(SettingsInterface.KeyAddresses, SettingsInterface.DefaultAddresses)
45 | .replace("46.101.249.188:26344", SettingsInterface.DefaultAddresses)
46 | settings.put(SettingsInterface.KeyAddresses, servers)
47 |
48 | serverThread.start()
49 | openAllConnections()
50 | }
51 |
52 | /**
53 | * Stops discovery and listening.
54 | */
55 | override def destroy(): Unit = {
56 | serverThread.cancel()
57 | connections.foreach(_.close())
58 | }
59 |
60 | private def openAllConnections(): Unit = {
61 | val addresses = settings.get(SettingsInterface.KeyAddresses, SettingsInterface.DefaultAddresses)
62 | .split(",")
63 | .map(_.trim())
64 | .filterNot(_.isEmpty)
65 |
66 | addresses.toList
67 | .foreach(openConnection)
68 | }
69 |
70 | def openConnection(addressPort: String): Unit = {
71 | val (address, port) =
72 | if (addressPort.contains(":")) {
73 | val split = addressPort.split(":")
74 | (split(0), split(1).toInt)
75 | } else
76 | (addressPort, InternetInterface.DefaultPort)
77 |
78 | openConnection(address, port)
79 | }
80 |
81 | /**
82 | * Opens connection to the specified IP address in client mode.
83 | */
84 | private def openConnection(address: String, port: Int): Unit = {
85 | logger.info(s"Attempting connection to $address:$port")
86 | Future {
87 | val socket = new Socket(InetAddress.getByName(address), port)
88 | val ct = new InternetConnectionThread(socket, crypto, onDisconnected, onReceiveMessage)
89 | connections += ct
90 | ct.start()
91 | }.onFailure { case e =>
92 | logger.warn("Failed to open connection to " + address + ":" + port, e)
93 | }
94 | }
95 |
96 | private def onConnected(connectionThread: InternetConnectionThread): Unit = {
97 | connections += connectionThread
98 | }
99 |
100 | private def onDisconnected(connectionThread: InternetConnectionThread): Unit = {
101 | getAddressForThread(connectionThread).foreach { ad =>
102 | logger.trace("Connection closed to " + ad)
103 | connections -= connectionThread
104 | addressDeviceMap -= ad
105 | val connectionDuration = new Duration(connectionThread.connectionOpened, DateTime.now)
106 | connectionHandler.onConnectionClosed(ad, connectionDuration)
107 |
108 | // If we aren't connected to any nodes, try to connect again.
109 | if (connections.isEmpty) {
110 | openAllConnections()
111 | }
112 | }
113 | }
114 |
115 | private def onReceiveMessage(msg: Message, thread: InternetConnectionThread): Unit = msg.body match {
116 | case info: ConnectionInfo =>
117 | val address = crypto.calculateAddress(info.key)
118 | if (address == crypto.localAddress) {
119 | logger.info("Address " + address + " is me, not connecting to myself")
120 | thread.close()
121 | return
122 | }
123 |
124 | // Service.onConnectionOpened sends message, so mapping already needs to be in place.
125 | addressDeviceMap += (address -> thread)
126 | if (!connectionHandler.onConnectionOpened(msg))
127 | addressDeviceMap -= address
128 | case _ =>
129 | connectionHandler.onMessageReceived(msg, getAddressForThread(thread).get)
130 | }
131 |
132 | private def getAddressForThread(thread: InternetConnectionThread) =
133 | addressDeviceMap.find(_._2 == thread).map(_._1)
134 |
135 | /**
136 | * Sends the message to nextHop.
137 | */
138 | override def send(nextHop: Address, msg: Message): Unit = {
139 | addressDeviceMap
140 | .filter(_._1 == nextHop || Address.Broadcast == nextHop)
141 | .foreach(_._2.send(msg))
142 | }
143 |
144 | /**
145 | * Returns all active Internet connections.
146 | */
147 | override def getConnections = addressDeviceMap.keySet
148 |
149 | def connectionChanged(): Unit = {
150 | FutureHelper {
151 | logger.info("Network has changed. Closing all connections and connecting to bootstrap nodes again")
152 | connections.foreach(_.close())
153 | openAllConnections()
154 | }
155 | }
156 |
157 | }
158 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.internet
2 |
3 | import java.io.IOException
4 | import java.net.ServerSocket
5 |
6 | import com.nutomic.ensichat.core.messages.Message
7 | import com.nutomic.ensichat.core.util.Crypto
8 | import com.typesafe.scalalogging.Logger
9 |
10 | class InternetServerThread(crypto: Crypto, port: Int,
11 | onConnected: (InternetConnectionThread) => Unit,
12 | onDisconnected: (InternetConnectionThread) => Unit,
13 | onReceive: (Message, InternetConnectionThread) => Unit) extends Thread {
14 |
15 | private val logger = Logger(this.getClass)
16 |
17 | private lazy val socket: Option[ServerSocket] = try {
18 | Option(new ServerSocket(port))
19 | } catch {
20 | case e: IOException =>
21 | logger.warn("Failed to create server socket", e)
22 | None
23 | }
24 |
25 | override def run(): Unit = {
26 | if (socket.isEmpty)
27 | return
28 |
29 | try {
30 | while (socket.get.isBound) {
31 | val connection = new InternetConnectionThread(socket.get.accept(), crypto, onDisconnected, onReceive)
32 | onConnected(connection)
33 | connection.start()
34 | }
35 | } catch {
36 | case e: IOException => logger.warn("Failed to accept connection", e)
37 | }
38 | }
39 |
40 | def cancel(): Unit = {
41 | try {
42 | socket.get.close()
43 | } catch {
44 | case e: IOException => logger.warn("Failed to close socket", e)
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/Message.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages
2 |
3 | import java.io.InputStream
4 | import java.security.spec.InvalidKeySpecException
5 |
6 | import com.nutomic.ensichat.core.messages
7 | import com.nutomic.ensichat.core.messages.body._
8 | import com.nutomic.ensichat.core.messages.header.{AbstractHeader, ContentHeader}
9 |
10 | object Message {
11 |
12 | /**
13 | * Orders messages by date, oldest messages first.
14 | */
15 | val Ordering = new Ordering[Message] {
16 | override def compare(m1: Message, m2: Message) = (m1.header, m2.header) match {
17 | case (h1: ContentHeader, h2: ContentHeader) =>
18 | h1.time.get.compareTo(h2.time.get)
19 | case _ => 0
20 | }
21 | }
22 |
23 | val Charset = "UTF-8"
24 |
25 | class ReadMessageException(message: String, throwable: Throwable)
26 | extends RuntimeException(message, throwable) {
27 | def this(message: String) = this(message, null)
28 | def this(throwable: Throwable) = this(null, throwable)
29 | }
30 |
31 | /**
32 | * Reads the entire message (header, crypto and body) into an object.
33 | */
34 | @throws(classOf[ReadMessageException])
35 | def read(stream: InputStream): Message = {
36 | try {
37 | val headerBytes = new Array[Byte](messages.header.MessageHeader.Length)
38 | stream.read(headerBytes, 0, messages.header.MessageHeader.Length)
39 | var (header: AbstractHeader, length) = messages.header.MessageHeader.read(headerBytes)
40 |
41 | var contentBytes = readStream(stream, length - header.length)
42 |
43 | if (header.isContentMessage) {
44 | val ret: (ContentHeader, Array[Byte]) = ContentHeader.read(header, contentBytes)
45 | header = ret._1
46 | contentBytes = ret._2
47 | }
48 |
49 | val (crypto, remaining) = CryptoData.read(contentBytes)
50 |
51 | val body =
52 | header.protocolType match {
53 | case ConnectionInfo.Type => ConnectionInfo.read(remaining)
54 | case RouteRequest.Type => RouteRequest.read(remaining)
55 | case RouteReply.Type => RouteReply.read(remaining)
56 | case RouteError.Type => RouteError.read(remaining)
57 | case PublicKeyRequest.Type => PublicKeyRequest.read(remaining)
58 | case PublicKeyReply.Type => PublicKeyReply.read(remaining)
59 | case _ => EncryptedBody(remaining)
60 | }
61 |
62 | new Message(header, crypto, body)
63 | } catch {
64 | case e @ (_ : OutOfMemoryError | _ : InvalidKeySpecException) =>
65 | throw new ReadMessageException(e)
66 | }
67 | }
68 |
69 | /**
70 | * Reads length bytes from stream and returns them.
71 | */
72 | private def readStream(stream: InputStream, length: Int): Array[Byte] = {
73 | val contentBytes = new Array[Byte](length)
74 |
75 | var numRead = 0
76 | do {
77 | numRead += stream.read(contentBytes, numRead, length - numRead)
78 | } while (numRead < length)
79 | contentBytes
80 | }
81 |
82 | }
83 |
84 | case class Message(header: AbstractHeader, crypto: CryptoData, body: MessageBody) {
85 |
86 | def this(header: AbstractHeader, body: MessageBody) =
87 | this(header, new CryptoData(None, None), body)
88 |
89 | def write = {
90 | header.write(body.length + crypto.length) ++ crypto.write ++ body.write
91 | }
92 |
93 | override def toString =
94 | s"Message(${header.origin.short}(${header.seqNum}) -> ${header.target.short}: $body)"
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/ConnectionInfo.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 | import java.security.spec.X509EncodedKeySpec
5 | import java.security.{KeyFactory, PublicKey}
6 |
7 | import com.nutomic.ensichat.core.util.{BufferUtils, Crypto}
8 |
9 | object ConnectionInfo {
10 |
11 | val Type = 0
12 |
13 | val HopLimit = 1
14 |
15 | /**
16 | * Constructs [[ConnectionInfo]] instance from byte array.
17 | */
18 | def read(array: Array[Byte]): ConnectionInfo = {
19 | val b = ByteBuffer.wrap(array)
20 | val length = BufferUtils.getUnsignedInt(b).toInt
21 | val encoded = new Array[Byte](length)
22 | b.get(encoded, 0, length)
23 |
24 | val factory = KeyFactory.getInstance(Crypto.PublicKeyAlgorithm)
25 | val key = factory.generatePublic(new X509EncodedKeySpec(encoded))
26 | new ConnectionInfo(key)
27 | }
28 |
29 | }
30 |
31 | /**
32 | * Holds a node's public key.
33 | */
34 | final case class ConnectionInfo(key: PublicKey) extends MessageBody {
35 |
36 | override def protocolType = ConnectionInfo.Type
37 |
38 | override def contentType = -1
39 |
40 | override def write: Array[Byte] = {
41 | val b = ByteBuffer.allocate(length)
42 | BufferUtils.putUnsignedInt(b, key.getEncoded.length)
43 | b.put(key.getEncoded)
44 | b.array()
45 | }
46 |
47 | override def length = 4 + key.getEncoded.length
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/CryptoData.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 | import java.util
5 |
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 |
8 | object CryptoData {
9 |
10 | /**
11 | * Constructs [[CryptoData]] instance from byte array.
12 | */
13 | def read(array: Array[Byte]): (CryptoData, Array[Byte]) = {
14 | val b = ByteBuffer.wrap(array)
15 | val signatureLength = BufferUtils.getUnsignedShort(b)
16 | val keyLength = BufferUtils.getUnsignedShort(b)
17 | val signature = new Array[Byte](signatureLength)
18 | b.get(signature, 0, signatureLength)
19 |
20 | val key =
21 | if (keyLength != 0) {
22 | val key = new Array[Byte](keyLength)
23 | b.get(key, 0, keyLength)
24 | Option(key)
25 | }
26 | else None
27 |
28 | val remaining = new Array[Byte](b.remaining())
29 | b.get(remaining, 0, b.remaining())
30 | (new CryptoData(Option(signature), key), remaining)
31 | }
32 |
33 | }
34 |
35 | /**
36 | * Holds the signature and (optional) key that are stored in a message.
37 | */
38 | final case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) {
39 |
40 | override def equals(a: Any): Boolean = a match {
41 | case o: CryptoData => util.Arrays.equals(signature.orNull, o.signature.orNull) &&
42 | util.Arrays.equals(key.orNull, o.key.orNull)
43 | case _ => false
44 | }
45 |
46 | /**
47 | * Writes this object into a new byte array.
48 | */
49 | def write: Array[Byte] = {
50 | val b = ByteBuffer.allocate(length)
51 | BufferUtils.putUnsignedShort(b, signature.get.length)
52 | BufferUtils.putUnsignedShort(b, keyLength)
53 | b.put(signature.get)
54 | if (key.nonEmpty) b.put(key.get)
55 | b.array()
56 | }
57 |
58 | def length = 4 + signature.get.length + keyLength
59 |
60 | private def keyLength = if (key.isDefined) key.get.length else 0
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/EncryptedBody.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | /**
4 | * Represents the data in an encrypted message body.
5 | */
6 | final case class EncryptedBody(data: Array[Byte]) extends MessageBody {
7 |
8 | override def protocolType = -1
9 |
10 | override def contentType = -1
11 |
12 | def write = data
13 |
14 | override def length = data.length
15 |
16 | override def toString = "EncryptedBody"
17 | }
18 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/MessageBody.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | /**
4 | * Holds the actual message content.
5 | */
6 | abstract class MessageBody {
7 |
8 | def protocolType: Int
9 |
10 | def contentType: Int
11 |
12 | /**
13 | * Writes the message contents to a byte array.
14 | */
15 | def write: Array[Byte]
16 |
17 | def length: Int
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/MessageReceived.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.util.BufferUtils
6 |
7 | object MessageReceived {
8 |
9 | val Type = 8
10 |
11 | /**
12 | * Constructs [[Text]] instance from byte array.
13 | */
14 | def read(array: Array[Byte]): MessageReceived = {
15 | val b = ByteBuffer.wrap(array)
16 | val messageId = BufferUtils.getUnsignedInt(b)
17 | new MessageReceived(messageId)
18 | }
19 |
20 | }
21 |
22 | /**
23 | * Holds a plain text message.
24 | */
25 | final case class MessageReceived(messageId: Long) extends MessageBody {
26 |
27 | override def protocolType = -1
28 |
29 | override def contentType = MessageReceived.Type
30 |
31 | override def write: Array[Byte] = {
32 | val b = ByteBuffer.allocate(length)
33 | // TODO: This should be putUnsignedLong, but doesn't seem possible in the JVM.
34 | // Alternatively, we could use signed ints instead.
35 | BufferUtils.putUnsignedInt(b, messageId)
36 | b.array()
37 | }
38 |
39 | override def length = 4
40 |
41 | override def equals(a: Any): Boolean = a match {
42 | case o: MessageReceived => messageId == o.messageId
43 | case _ => false
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/PublicKeyReply.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 | import java.security.spec.X509EncodedKeySpec
5 | import java.security.{KeyFactory, PublicKey}
6 |
7 | import com.nutomic.ensichat.core.util.BufferUtils
8 | import com.nutomic.ensichat.core.util.Crypto
9 |
10 | object PublicKeyReply {
11 |
12 | val Type = 6
13 |
14 | /**
15 | * Constructs [[ConnectionInfo]] instance from byte array.
16 | */
17 | def read(array: Array[Byte]): PublicKeyReply = {
18 | val b = ByteBuffer.wrap(array)
19 | val length = BufferUtils.getUnsignedInt(b).toInt
20 | val encoded = new Array[Byte](length)
21 | b.get(encoded, 0, length)
22 |
23 | val factory = KeyFactory.getInstance(Crypto.PublicKeyAlgorithm)
24 | val key = factory.generatePublic(new X509EncodedKeySpec(encoded))
25 | new PublicKeyReply(key)
26 | }
27 |
28 | }
29 |
30 | case class PublicKeyReply(key: PublicKey) extends MessageBody {
31 |
32 | override def protocolType = PublicKeyRequest.Type
33 |
34 | override def contentType = -1
35 |
36 | override def write: Array[Byte] = {
37 | val b = ByteBuffer.allocate(length)
38 | BufferUtils.putUnsignedInt(b, key.getEncoded.length)
39 | b.put(key.getEncoded)
40 | b.array()
41 | }
42 |
43 | override def length = 4 + key.getEncoded.length
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/PublicKeyRequest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.routing.Address
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 |
8 | object PublicKeyRequest {
9 |
10 | val Type = 5
11 |
12 | /**
13 | * Constructs [[Text]] instance from byte array.
14 | */
15 | def read(array: Array[Byte]): PublicKeyRequest = {
16 | val b = ByteBuffer.wrap(array)
17 | val length = BufferUtils.getUnsignedInt(b).toInt
18 | val bytes = new Array[Byte](length)
19 | b.get(bytes, 0, length)
20 | new PublicKeyRequest(new Address(bytes))
21 | }
22 |
23 | }
24 |
25 | case class PublicKeyRequest(address: Address) extends MessageBody {
26 |
27 | require(address != Address.Broadcast, "")
28 | require(address != Address.Null, "")
29 |
30 | override def protocolType = PublicKeyRequest.Type
31 |
32 | override def contentType = -1
33 |
34 | override def write: Array[Byte] = {
35 | val b = ByteBuffer.allocate(length)
36 | val bytes = address.bytes
37 | BufferUtils.putUnsignedInt(b, bytes.length)
38 | b.put(bytes)
39 | b.array()
40 | }
41 |
42 |
43 | override def equals(a: Any): Boolean = a match {
44 | case o: PublicKeyRequest => address == o.address
45 | case _ => false
46 | }
47 |
48 | override def length = 4 + address.bytes.length
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/RouteError.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.routing.Address
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 |
8 | private[core] object RouteError {
9 |
10 | val Type = 4
11 |
12 | /**
13 | * Constructs [[RouteError]] instance from byte array.
14 | */
15 | def read(array: Array[Byte]): RouteError = {
16 | val b = ByteBuffer.wrap(array)
17 | val address = new Address(BufferUtils.getByteArray(b, Address.Length))
18 | val seqNum = b.getInt
19 | new RouteError(address, seqNum)
20 | }
21 |
22 | }
23 |
24 | private[core] case class RouteError(address: Address, seqNum: Int) extends MessageBody {
25 |
26 | override def protocolType = RouteReply.Type
27 |
28 | override def contentType = -1
29 |
30 | override def write: Array[Byte] = {
31 | val b = ByteBuffer.allocate(length)
32 | b.put(address.bytes)
33 | b.putInt(seqNum)
34 | b.array()
35 | }
36 |
37 | override def length = Address.Length + 4
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/RouteReply.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.util.BufferUtils
6 |
7 | private[core] object RouteReply {
8 |
9 | val Type = 3
10 |
11 | /**
12 | * Constructs [[RouteReply]] instance from byte array.
13 | */
14 | def read(array: Array[Byte]): RouteReply = {
15 | val b = ByteBuffer.wrap(array)
16 | val targSeqNum = BufferUtils.getUnsignedShort(b)
17 | val targMetric = BufferUtils.getUnsignedShort(b)
18 | new RouteReply(targSeqNum, targMetric)
19 | }
20 |
21 | }
22 |
23 | /**
24 | * Sends information about a route.
25 | *
26 | * Note that the fields are named different than described in AODVv2. There, targSeqNum and
27 | * targMetric are used to describe the seqNum and metric of the node sending the route reply. In
28 | * Ensichat, we use originSeqNum and originMetric instead, to stay consistent with the header
29 | * fields. That means header.origin, originSeqNum and originMetric all refer to the node sending
30 | * this message.
31 | *
32 | * @param originSeqNum The current sequence number of the node sending this message.
33 | * @param originMetric The metric of the current route to the sending node.
34 | */
35 | private[core] case class RouteReply(originSeqNum: Int, originMetric: Int) extends MessageBody {
36 |
37 | override def protocolType = RouteReply.Type
38 |
39 | override def contentType = -1
40 |
41 | override def write: Array[Byte] = {
42 | val b = ByteBuffer.allocate(length)
43 | BufferUtils.putUnsignedShort(b, originSeqNum)
44 | BufferUtils.putUnsignedShort(b, originMetric)
45 | b.array()
46 | }
47 |
48 | override def length = 4
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/RouteRequest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.routing.Address
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 |
8 | private[core] object RouteRequest {
9 |
10 | val Type = 2
11 |
12 | /**
13 | * Constructs [[RouteRequest]] instance from byte array.
14 | */
15 | def read(array: Array[Byte]): RouteRequest = {
16 | val b = ByteBuffer.wrap(array)
17 | val requested = new Address(BufferUtils.getByteArray(b, Address.Length))
18 | val origSeqNum = BufferUtils.getUnsignedShort(b)
19 | val originMetric = BufferUtils.getUnsignedShort(b)
20 | val targSeqNum = b.getInt()
21 | new RouteRequest(requested, origSeqNum, targSeqNum, originMetric)
22 | }
23 |
24 | }
25 |
26 | private[core] case class RouteRequest(requested: Address, originSeqNum: Int, targSeqNum: Int, originMetric: Int)
27 | extends MessageBody {
28 |
29 | override def protocolType = RouteRequest.Type
30 |
31 | override def contentType = -1
32 |
33 | override def write: Array[Byte] = {
34 | val b = ByteBuffer.allocate(length)
35 | b.put(requested.bytes)
36 | BufferUtils.putUnsignedShort(b, originSeqNum)
37 | BufferUtils.putUnsignedShort(b, originMetric)
38 | b.putInt(targSeqNum)
39 | b.array()
40 | }
41 |
42 | override def length = 8 + Address.Length
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/Text.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.messages.Message
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 |
8 | object Text {
9 |
10 | val Type = 6
11 |
12 | /**
13 | * Constructs [[Text]] instance from byte array.
14 | */
15 | def read(array: Array[Byte]): Text = {
16 | val b = ByteBuffer.wrap(array)
17 | val length = BufferUtils.getUnsignedInt(b).toInt
18 | val bytes = new Array[Byte](length)
19 | b.get(bytes, 0, length)
20 | new Text(new String(bytes, Message.Charset))
21 | }
22 |
23 | }
24 |
25 | /**
26 | * Holds a plain text message.
27 | */
28 | final case class Text(text: String) extends MessageBody {
29 |
30 | override def protocolType = -1
31 |
32 | override def contentType = Text.Type
33 |
34 | override def write: Array[Byte] = {
35 | val b = ByteBuffer.allocate(length)
36 | val bytes = text.getBytes(Message.Charset)
37 | BufferUtils.putUnsignedInt(b, bytes.length)
38 | b.put(bytes)
39 | b.array()
40 | }
41 |
42 | override def length = 4 + text.getBytes(Message.Charset).length
43 |
44 | override def equals(a: Any): Boolean = a match {
45 | case o: Text => text == text
46 | case _ => false
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/body/UserInfo.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.messages.Message
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 |
8 | object UserInfo {
9 |
10 | val Type = 7
11 |
12 | /**
13 | * Constructs [[UserInfo]] instance from byte array.
14 | */
15 | def read(array: Array[Byte]): UserInfo = {
16 | val bb = ByteBuffer.wrap(array)
17 | new UserInfo(getValue(bb), getValue(bb))
18 | }
19 |
20 | private def getValue(bb: ByteBuffer): String = {
21 | val length = BufferUtils.getUnsignedInt(bb).toInt
22 | val bytes = new Array[Byte](length)
23 | bb.get(bytes, 0, length)
24 | new String(bytes, Message.Charset)
25 | }
26 |
27 | }
28 |
29 | /**
30 | * Holds display name and status of the sender.
31 | */
32 | final case class UserInfo(name: String, status: String) extends MessageBody {
33 |
34 | override def protocolType = -1
35 |
36 | override def contentType = UserInfo.Type
37 |
38 | override def write: Array[Byte] = {
39 | val b = ByteBuffer.allocate(length)
40 | put(b, name)
41 | put(b, status)
42 | b.array()
43 | }
44 |
45 | def put(b: ByteBuffer, value: String): ByteBuffer = {
46 | val bytes = value.getBytes(Message.Charset)
47 | BufferUtils.putUnsignedInt(b, bytes.length)
48 | b.put(bytes)
49 | }
50 |
51 | override def length = 8 + name.getBytes(Message.Charset).length +
52 | status.getBytes(Message.Charset).length
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/header/AbstractHeader.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.header
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.routing.Address
6 | import com.nutomic.ensichat.core.util.BufferUtils
7 | import org.joda.time.DateTime
8 |
9 | object AbstractHeader {
10 |
11 | val InitialForwardingTokens = 3
12 |
13 | val MaxForwardingTokens = 6
14 |
15 | val Version = 0
16 |
17 | private[header] val Length = 10 + 2 * Address.Length
18 |
19 | }
20 |
21 | /**
22 | * Contains the header fields and functionality that are used both in [[MessageHeader]] and
23 | * [[ContentHeader]].
24 | *
25 | * The fields messageId and time are only set in [[ContentHeader]].
26 | */
27 | trait AbstractHeader {
28 |
29 | def protocolType: Int
30 | def tokens: Int
31 | def hopCount: Int
32 | def origin: Address
33 | def target: Address
34 | def seqNum: Int
35 | def messageId: Option[Long] = None
36 | def time: Option[DateTime] = None
37 |
38 | /**
39 | * Writes the header to byte array.
40 | */
41 | def write(contentLength: Int): Array[Byte] = {
42 | val b = ByteBuffer.allocate(AbstractHeader.Length)
43 |
44 | BufferUtils.putUnsignedByte(b, AbstractHeader.Version)
45 | BufferUtils.putUnsignedByte(b, protocolType)
46 | BufferUtils.putUnsignedByte(b, tokens)
47 | BufferUtils.putUnsignedByte(b, hopCount)
48 |
49 | BufferUtils.putUnsignedInt(b, length + contentLength)
50 | b.put(origin.bytes)
51 | b.put(target.bytes)
52 |
53 | BufferUtils.putUnsignedShort(b, seqNum)
54 |
55 | b.array()
56 | }
57 |
58 | /**
59 | * Returns true if this object is an instance of [[ContentHeader]].
60 | */
61 | def isContentMessage = protocolType == ContentHeader.ContentMessageType
62 |
63 | def length: Int
64 |
65 | override def equals(a: Any): Boolean = a match {
66 | case o: AbstractHeader =>
67 | protocolType == o.protocolType &&
68 | tokens == o.tokens &&
69 | hopCount == o.hopCount &&
70 | origin == o.origin &&
71 | target == o.target &&
72 | seqNum == o.seqNum
73 | case _ => false
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/header/ContentHeader.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.header
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.messages.header
6 | import com.nutomic.ensichat.core.routing.Address
7 | import com.nutomic.ensichat.core.util.BufferUtils
8 | import org.joda.time.DateTime
9 |
10 | object ContentHeader {
11 |
12 | val Length = 10
13 |
14 | val ContentMessageType = 255
15 |
16 | val SeqNumRange = 0 until 1 << 16
17 |
18 | /**
19 | * Constructs [[MessageHeader]] from byte array.
20 | */
21 | def read(mh: header.AbstractHeader, bytes: Array[Byte]): (ContentHeader, Array[Byte]) = {
22 | val b = ByteBuffer.wrap(bytes)
23 |
24 | val contentType = BufferUtils.getUnsignedShort(b)
25 | val messageId = BufferUtils.getUnsignedInt(b)
26 | val time = BufferUtils.getUnsignedInt(b)
27 |
28 | val ch = new ContentHeader(mh.origin, mh.target, mh.seqNum, contentType, Some(messageId),
29 | Some(new DateTime(time * 1000)), mh.tokens, mh.hopCount)
30 |
31 | val remaining = new Array[Byte](b.remaining())
32 | b.get(remaining, 0, b.remaining())
33 | (ch, remaining)
34 | }
35 |
36 | }
37 |
38 | /**
39 | * Header for user-sent messages.
40 | *
41 | * This is [[header.AbstractHeader]] with messageId and time fields set.
42 | */
43 | final case class ContentHeader(override val origin: Address,
44 | override val target: Address,
45 | override val seqNum: Int,
46 | contentType: Int,
47 | override val messageId: Some[Long],
48 | override val time: Some[DateTime],
49 | override val tokens: Int,
50 | override val hopCount: Int = 0)
51 | extends header.AbstractHeader {
52 |
53 | override val protocolType = ContentHeader.ContentMessageType
54 |
55 | /**
56 | * Writes the header to byte array.
57 | */
58 | override def write(contentLength: Int): Array[Byte] = {
59 | val b = ByteBuffer.allocate(length)
60 |
61 | b.put(super.write(contentLength))
62 |
63 | BufferUtils.putUnsignedShort(b, contentType)
64 | BufferUtils.putUnsignedInt(b, messageId.get)
65 | BufferUtils.putUnsignedInt(b, time.get.getMillis / 1000)
66 |
67 | b.array()
68 | }
69 |
70 | override def length = header.AbstractHeader.Length + ContentHeader.Length
71 |
72 | override def equals(a: Any): Boolean = a match {
73 | case o: ContentHeader =>
74 | super.equals(a) &&
75 | contentType == o.contentType &&
76 | messageId == o.messageId &&
77 | time.get.getMillis / 1000 == o.time.get.getMillis / 1000
78 | case _ => false
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/messages/header/MessageHeader.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.header
2 |
3 | import java.nio.ByteBuffer
4 |
5 | import com.nutomic.ensichat.core.messages.Message.ReadMessageException
6 | import com.nutomic.ensichat.core.messages.{Message, header}
7 | import com.nutomic.ensichat.core.routing.Address
8 | import com.nutomic.ensichat.core.util.BufferUtils
9 |
10 | object MessageHeader {
11 |
12 | val Length = header.AbstractHeader.Length
13 |
14 | /**
15 | * Constructs header from byte array.
16 | *
17 | * @return The header and the message length in bytes.
18 | */
19 | @throws(classOf[ReadMessageException])
20 | def read(bytes: Array[Byte]): (MessageHeader, Int) = {
21 | val b = ByteBuffer.wrap(bytes, 0, MessageHeader.Length)
22 |
23 | val version = BufferUtils.getUnsignedByte(b)
24 | if (version != header.AbstractHeader.Version)
25 | throw new ReadMessageException("Failed to parse message with unsupported version " + version)
26 | val protocolType = BufferUtils.getUnsignedByte(b)
27 | val tokens = BufferUtils.getUnsignedByte(b)
28 | if (tokens > header.AbstractHeader.MaxForwardingTokens)
29 | throw new ReadMessageException(s"Received message with too many forwarding tokens ($tokens tokens)")
30 | val hopCount = BufferUtils.getUnsignedByte(b)
31 |
32 | val length = BufferUtils.getUnsignedInt(b)
33 | if (length < Length)
34 | throw new ReadMessageException("Received message with invalid length " + length)
35 | val origin = new Address(BufferUtils.getByteArray(b, Address.Length))
36 | val target = new Address(BufferUtils.getByteArray(b, Address.Length))
37 |
38 | val seqNum = BufferUtils.getUnsignedShort(b)
39 |
40 | (new MessageHeader(protocolType, origin, target, seqNum, tokens, hopCount), length.toInt)
41 | }
42 |
43 | }
44 |
45 | /**
46 | * First part of any message, used for routing.
47 | *
48 | * This is the same as [[header.AbstractHeader]].
49 | */
50 | final case class MessageHeader(override val protocolType: Int,
51 | override val origin: Address,
52 | override val target: Address,
53 | override val seqNum: Int,
54 | override val tokens: Int,
55 | override val hopCount: Int = 0)
56 | extends header.AbstractHeader {
57 |
58 | def length: Int = MessageHeader.Length
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/routing/Address.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | object Address {
4 |
5 | val Length = 32
6 |
7 | /**
8 | * Number of characters between each pair of dashes in [[Address.toString]].
9 | */
10 | val GroupLength = 8
11 |
12 | // 32 bytes, all ones
13 | // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
14 | val Broadcast = new Address("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
15 |
16 | // 32 bytes, all zeros
17 | // 0x0000000000000000000000000000000000000000000000000000000000000000
18 | val Null = new Address("0000000000000000000000000000000000000000000000000000000000000000")
19 |
20 | }
21 |
22 | /**
23 | * Holds a device address and provides conversion methods.
24 | *
25 | * @param bytes SHA-256 hash of the node's public key.
26 | */
27 | final case class Address(bytes: Array[Byte]) {
28 |
29 | require(bytes.length == Address.Length, "Invalid address length (was " + bytes.length + ")")
30 |
31 | /**
32 | * Parses address from string. Dash characters ("-") are ignored.
33 | */
34 | def this(hex: String) {
35 | this(hex
36 | .replace("-", "")
37 | .sliding(2, 2)
38 | .map(Integer.parseInt(_, 16).toByte)
39 | .toArray)
40 | }
41 |
42 | override def hashCode = java.util.Arrays.hashCode(bytes)
43 |
44 | override def equals(a: Any) = a match {
45 | case o: Address => bytes.deep == o.bytes.deep
46 | case _ => false
47 | }
48 |
49 | /**
50 | * Converts address to a string, with groups seperated by dashes.
51 | */
52 | override def toString =
53 | bytes
54 | .map("%02X".format(_))
55 | .mkString
56 | .grouped(Address.GroupLength)
57 | .reduce(_ + "-" + _)
58 |
59 | /**
60 | * Returns shortened address, useful for debugging.
61 | */
62 | def short = toString.split("-").head
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/routing/LocalRoutesInfo.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import com.nutomic.ensichat.core.routing.LocalRoutesInfo._
4 | import org.joda.time.{DateTime, Duration}
5 |
6 | private[core] object LocalRoutesInfo {
7 |
8 | private val ActiveInterval = Duration.standardSeconds(5)
9 |
10 | /**
11 | * [[RouteStates.Idle]]:
12 | * A route that is known, but has not been used in the last [[ActiveInterval]].
13 | * [[RouteStates.Active]]:
14 | * A route that is known, and has been used in the last [[ActiveInterval]].
15 | * [[RouteStates.Invalid]]:
16 | * A route that has been expired or lost, may not be used for forwarding.
17 | * RouteStates.Unconfirmed is not required as connections are always bidirectional.
18 | */
19 | object RouteStates extends Enumeration {
20 | type RouteStates = Value
21 | val Idle, Active, Invalid = Value
22 | }
23 |
24 | }
25 |
26 | /**
27 | * This class contains information about routes available to this node.
28 | *
29 | * See AODVv2-13 4.5 (Local Route Set), -> implemented
30 | * 6.9 (Local Route Set Maintenance) -> implemented (hopefully correct)
31 | */
32 | private[core] class LocalRoutesInfo(activeConnections: () => Set[Address]) {
33 |
34 | import RouteStates._
35 |
36 | private val MaxSeqnumLifetime = Duration.standardSeconds(300)
37 | // TODO: this can probably be much higher because of infrequent topology changes between internet nodes
38 | private val MaxIdleTime = Duration.standardSeconds(300)
39 |
40 |
41 | /**
42 | * Holds information about a local route.
43 | *
44 | * @param destination The destination address that can be reached with this route.
45 | * @param seqNum Sequence number of the last route message that updated this entry.
46 | * @param nextHop The next hop on the path towards destination.
47 | * @param lastUsed The time this route was last used to forward a message.
48 | * @param lastSeqNumUpdate The time seqNum was last updated.
49 | * @param metric The number of hops towards destination using this route.
50 | * @param state The last known state of the route.
51 | */
52 | case class RouteEntry(destination: Address, seqNum: Int, nextHop: Address, lastUsed: DateTime,
53 | lastSeqNumUpdate: DateTime, metric: Int, state: RouteStates)
54 |
55 | private var routes = Set[RouteEntry]()
56 |
57 | def addRoute(destination: Address, seqNum: Int, nextHop: Address, metric: Int): Unit = {
58 | val entry = RouteEntry(destination, seqNum, nextHop, new DateTime(0), DateTime.now, metric, Idle)
59 | routes += entry
60 | }
61 |
62 | /**
63 | * Returns a list of all known routes (excluding invalid), ordered by best metric.
64 | */
65 | def getAllAvailableRoutes: List[RouteEntry] = {
66 | handleTimeouts()
67 | val neighbors = activeConnections()
68 | .map(c => new RouteEntry(c, 0, c, DateTime.now, DateTime.now, 1, Idle))
69 | (neighbors ++ routes).toList
70 | .filter(r => r.state != Invalid)
71 | .sortWith(_.metric < _.metric)
72 | }
73 |
74 | def getRoute(destination: Address): Option[RouteEntry] = {
75 | val r = getAllAvailableRoutes
76 | .find( r => r.destination == destination)
77 |
78 | if (r.isDefined && routes.contains(r.get))
79 | routes = routes -- r + r.get.copy(state = Active, lastUsed = DateTime.now)
80 | r
81 | }
82 |
83 | /**
84 | *
85 | * @param address The address which can't be reached any more.
86 | * @return The set of active destinations that can't be reached anymore.
87 | */
88 | def connectionClosed(address: Address): Set[Address] = {
89 | handleTimeouts()
90 |
91 | val affectedDestinations =
92 | routes
93 | .filter(r => r.state == Active && (r.nextHop == address || r.destination == address))
94 | .map(_.destination)
95 |
96 | routes = routes.map { r =>
97 | if (r.nextHop == address || r.destination == address)
98 | r.copy(state = Invalid)
99 | else
100 | r
101 | }
102 |
103 | affectedDestinations
104 | }
105 |
106 | private def handleTimeouts(): Unit = {
107 | routes = routes
108 | // Delete routes after max lifetime.
109 | .map { r =>
110 | if (DateTime.now.isAfter(r.lastSeqNumUpdate.plus(MaxSeqnumLifetime)))
111 | r.copy(seqNum = 0)
112 | else
113 | r
114 | }
115 | // Set routes to invalid after max idle time.
116 | .map { r =>
117 | if (DateTime.now.isAfter(r.lastSeqNumUpdate.plus(MaxIdleTime)))
118 | r.copy(state = Invalid)
119 | else
120 | r
121 | }
122 | }
123 |
124 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/routing/MessageBuffer.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import java.util.{Timer, TimerTask}
4 |
5 | import com.nutomic.ensichat.core.messages.Message
6 | import com.typesafe.scalalogging.Logger
7 | import org.joda.time.{DateTime, Duration}
8 |
9 | /**
10 | * Contains messages that couldn't be forwarded because we don't know a route.
11 | */
12 | class MessageBuffer(localAddress: Address, retryMessageSending: (Address) => Unit) {
13 |
14 | private val logger = Logger(this.getClass)
15 |
16 | /**
17 | * The maximum number of times we retry to deliver a message.
18 | */
19 | private val MaxRetryCount = 6
20 |
21 | private val timer = new Timer()
22 |
23 | private case class BufferEntry(message: Message, added: DateTime, retryCount: Int)
24 |
25 | private var values = Set[BufferEntry]()
26 |
27 | private var isStopped = false
28 |
29 | def stop(): Unit = {
30 | isStopped = true
31 | timer.cancel()
32 | }
33 |
34 | def addMessage(msg: Message): Unit = {
35 | // For old messages added back from database, find their retry count from send time and offset.
36 | val retryCount =
37 | (0 to 6).find { i =>
38 | msg.header.time.get.plus(calculateNextRetryOffset(i)).isAfter(DateTime.now)
39 | }
40 | .getOrElse(6)
41 | val newEntry = new BufferEntry(msg, DateTime.now, retryCount)
42 | values += newEntry
43 | retryMessage(newEntry)
44 | logger.info(s"Added message to buffer, now ${values.size} messages stored")
45 | }
46 |
47 | /**
48 | * Calculates the duration until the next retry, measured from the time the message was added.
49 | *
50 | * retryCount is limited to a value of 6.
51 | */
52 | private def calculateNextRetryOffset(retryCount: Int) =
53 | Duration.standardSeconds(10 ^ Math.min(6, retryCount + 1))
54 |
55 | /**
56 | * Starts a timer to retry the route discovery.
57 | *
58 | * The delivery will not be retried if the [[stop]] was called, the message has timed out from
59 | * the buffer, the message was sent, or a newer message for the same destination was added.
60 | */
61 | private def retryMessage(entry: BufferEntry) {
62 | timer.schedule(new TimerTask {
63 | override def run(): Unit = {
64 | if (isStopped)
65 | return
66 |
67 | // New entry was added for the same destination, don't retry here any more.
68 | val newerEntryExists = values
69 | .filter(_.message.header.target == entry.message.header.target)
70 | .map(_.added)
71 | .exists(_.isAfter(entry.added))
72 | if (newerEntryExists)
73 | return
74 |
75 | // Don't retry if message was sent in the mean time, or message timed out.
76 | handleTimeouts()
77 | if (!values.map(_.message).contains(entry.message))
78 | return
79 |
80 | retryMessageSending(entry.message.header.target)
81 | val updated = entry.copy(retryCount = entry.retryCount + 1)
82 | retryMessage(updated)
83 | }
84 | }, calculateNextRetryOffset(entry.retryCount).getMillis)
85 | }
86 |
87 | /**
88 | * Returns all buffered messages for destination, and removes them from the buffer.
89 | */
90 | def getMessages(destination: Address): Set[Message] = {
91 | handleTimeouts()
92 | val ret = values.filter(_.message.header.target == destination)
93 | values --= ret
94 | ret.map(_.message)
95 | }
96 |
97 | def getAllMessages: Set[Message] = values.map(_.message)
98 |
99 | private def handleTimeouts(): Unit = {
100 | val sizeBefore = values.size
101 | values = values.filter { e =>
102 | e.retryCount < MaxRetryCount && e.message.header.origin != localAddress
103 | }
104 | val difference = values.size - sizeBefore
105 | if (difference > 0)
106 | logger.info(s"Removed $difference message(s), now ${values.size} messages stored")
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/routing/RouteMessageInfo.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import com.nutomic.ensichat.core.messages.Message
4 | import com.nutomic.ensichat.core.messages.body.{RouteReply, RouteRequest}
5 | import org.joda.time.{DateTime, Duration}
6 |
7 | /**
8 | * Contains information about AODVv2 control messages that have been received.
9 | *
10 | * This class handles Route Request and Route Reply messages (referred to as "route messages").
11 | *
12 | * See AODVv2-13 4.6 (Multicast Route Message Table), -> implemented
13 | * 6.8 (Surpressing Redundant Messages Using the Multicast Route Message Table) -> implemented (hopefully correct)
14 | */
15 | private[core] class RouteMessageInfo {
16 |
17 | private val MaxSeqnumLifetime = Duration.standardSeconds(300)
18 |
19 | /**
20 | * @param messageType Either [[RouteRequest.Type]] or [[RouteReply.Type]].
21 | * @param origAddress Source address of the route message triggering the route request.
22 | * @param targAddress Destination address of the route message triggering the route request.
23 | * @param origSeqNum Sequence number associated with the route to [[origAddress]], if route
24 | * message is an RREQ.
25 | * @param targSeqNum Sequence number associated with the route to [[targAddress]], if present in
26 | * the route message.
27 | * @param metric Metric value received in the route message.
28 | * @param timestamp Last time this entry was updated.
29 | */
30 | private case class RouteMessageEntry(messageType: Int, origAddress: Address,
31 | targAddress: Address, origSeqNum: Int, targSeqNum: Int,
32 | metric: Int, timestamp: DateTime)
33 |
34 | private var entries = Set[RouteMessageEntry]()
35 |
36 | private def addEntry(msg: Message): Unit = msg.body match {
37 | case rreq: RouteRequest =>
38 | entries += new RouteMessageEntry(RouteRequest.Type, msg.header.origin, msg.header.target,
39 | msg.header.seqNum, rreq.targSeqNum, rreq.originMetric,
40 | DateTime.now)
41 | case rrep: RouteReply =>
42 | entries += new RouteMessageEntry(RouteReply.Type, msg.header.origin, msg.header.target,
43 | msg.header.seqNum, rrep.originSeqNum, rrep.originMetric,
44 | DateTime.now)
45 | }
46 |
47 | def isMessageRedundant(msg: Message): Boolean = {
48 | handleTimeouts()
49 | val existingEntry =
50 | entries.find { e =>
51 | val haveEntry = e.messageType == msg.header.protocolType &&
52 | e.origAddress == msg.header.origin && e.targAddress == msg.header.target
53 |
54 | val (metric, seqNumComparison) = msg.body match {
55 | case rreq: RouteRequest => (rreq.originMetric, Router.compare(rreq.originSeqNum, e.origSeqNum))
56 | case rrep: RouteReply => (rrep.originMetric, Router.compare(rrep.originSeqNum, e.targSeqNum))
57 | }
58 | val isMetricBetter = e.metric < metric
59 | haveEntry && (seqNumComparison > 0 || (seqNumComparison == 0 && isMetricBetter))
60 | }
61 | if (existingEntry.isDefined)
62 | entries = entries - existingEntry.get
63 |
64 | addEntry(msg)
65 |
66 | existingEntry.isDefined
67 | }
68 |
69 | private def handleTimeouts(): Unit = {
70 | entries = entries.filter { e =>
71 | DateTime.now.isBefore(e.timestamp.plus(MaxSeqnumLifetime))
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/routing/Router.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import java.util.Comparator
4 |
5 | import com.nutomic.ensichat.core.messages.header.ContentHeader
6 | import com.nutomic.ensichat.core.messages.{Message, header}
7 |
8 | object Router extends Comparator[Int] {
9 |
10 | private val HopLimit = 20
11 |
12 | /**
13 | * Compares which sequence number is newer.
14 | *
15 | * @return 1 if lhs is newer, -1 if rhs is newer, 0 if they are equal.
16 | */
17 | override def compare(lhs: Int, rhs: Int): Int = {
18 | if (lhs == rhs)
19 | 0
20 | // True if [[rhs]] is between {{{MessageHeader.SeqNumRange.size / 2}}} and
21 | // [[MessageHeader.SeqNumRange.size]].
22 | else if (lhs > ContentHeader.SeqNumRange.size / 2) {
23 | // True if [[rhs]] is between {{{lhs - MessageHeader.SeqNumRange.size / 2}}} and [[lhs]].
24 | if (lhs - ContentHeader.SeqNumRange.size / 2 < rhs && rhs < lhs) 1 else -1
25 | } else {
26 | // True if [[rhs]] is *not* between [[lhs]] and {{{lhs + MessageHeader.SeqNumRange.size / 2}}}.
27 | if (rhs < lhs || rhs > lhs + ContentHeader.SeqNumRange.size / 2) 1 else -1
28 | }
29 | }
30 | }
31 |
32 | /**
33 | * Forwards messages to all connected devices.
34 | */
35 | private[core] class Router(routesInfo: LocalRoutesInfo, send: (Address, Message) => Unit,
36 | noRouteFound: (Message) => Unit) {
37 |
38 | private var messageSeen = Set[(Address, Int)]()
39 |
40 | /**
41 | * Returns true if we have received the same message before.
42 | */
43 | private[core] def isMessageSeen(msg: Message): Boolean = {
44 | val info = (msg.header.origin, msg.header.seqNum)
45 | val seen = messageSeen.contains(info)
46 | markMessageSeen(info)
47 | seen
48 | }
49 |
50 | /**
51 | * Sends message to all connected devices. Should only be called if [[isMessageSeen()]] returns
52 | * true.
53 | */
54 | def forwardMessage(msg: Message, nextHopOption: Option[Address] = None): Unit = {
55 | if (msg.header.hopCount + 1 >= Router.HopLimit)
56 | return
57 |
58 | val nextHop = nextHopOption.getOrElse(msg.header.target)
59 |
60 | if (nextHop == Address.Broadcast) {
61 | send(nextHop, msg)
62 | return
63 | }
64 |
65 | routesInfo.getRoute(nextHop).map(_.nextHop) match {
66 | case Some(a) =>
67 | send(a, incHopCount(msg))
68 | markMessageSeen((msg.header.origin, msg.header.seqNum))
69 | case None =>
70 | if (msg.header.isInstanceOf[ContentHeader])
71 | noRouteFound(msg)
72 | }
73 | }
74 |
75 | private def markMessageSeen(info: (Address, Int)): Unit = {
76 | trimMessageSeen(info._1, info._2)
77 | messageSeen += info
78 | }
79 |
80 | /**
81 | * Returns msg with hop count increased by one.
82 | */
83 | private def incHopCount(msg: Message): Message = {
84 | val updatedHeader = msg.header match {
85 | case ch: ContentHeader => ch.copy(hopCount = ch.hopCount + 1)
86 | case mh: header.MessageHeader => mh.copy(hopCount = mh.hopCount + 1)
87 | }
88 | new Message(updatedHeader, msg.crypto, msg.body)
89 | }
90 |
91 | /**
92 | * Removes old entries from [[messageSeen]].
93 | *
94 | * Only the last half of possible sequence number values are kept. For example, if sequence
95 | * numbers are between 0 and 10, and a new message with sequence number 6 arrives, all entries
96 | * for messages with sequence numbers outside [2, 6] are removed.
97 | */
98 | private def trimMessageSeen(a1: Address, s1: Int): Unit = {
99 | messageSeen = messageSeen.filter { case (a2, s2) =>
100 | if (a1 != a2)
101 | true
102 |
103 | else
104 | Router.compare(s1, s2) > 0
105 | }
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import java.nio.ByteBuffer
4 |
5 | /**
6 | * Provides various helper methods for [[ByteBuffer]].
7 | */
8 | private[core] object BufferUtils {
9 |
10 | def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
11 |
12 | def putUnsignedByte(bb: ByteBuffer, value: Int) = bb.put((value & 0xff).toByte)
13 |
14 | def getUnsignedShort(bb: ByteBuffer): Int = bb.getShort & 0xffff
15 |
16 | def putUnsignedShort(bb: ByteBuffer, value: Int) = bb.putShort((value & 0xffff).toShort)
17 |
18 | def getUnsignedInt(bb: ByteBuffer): Long = bb.getInt.toLong & 0xffffffffL
19 |
20 | def putUnsignedInt(bb: ByteBuffer, value: Long) = bb.putInt((value & 0xffffffffL).toInt)
21 |
22 | /**
23 | * Reads a byte array with the given length and returns it.
24 | */
25 | def getByteArray(bb: ByteBuffer, numBytes: Int): Array[Byte] = {
26 | val b = new Array[Byte](numBytes)
27 | bb.get(b, 0, numBytes)
28 | b
29 | }
30 |
31 | def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import com.typesafe.scalalogging.Logger
4 |
5 | import scala.concurrent.{ExecutionContext, Future}
6 |
7 | /**
8 | * Wraps [[Future]], so that exceptions are always thrown.
9 | *
10 | * @see https://github.com/saturday06/gradle-android-scala-plugin/issues/56
11 | */
12 | object FutureHelper {
13 |
14 | private val logger = Logger(this.getClass)
15 |
16 | def apply[A](action: => A)(implicit executor: ExecutionContext): Future[A] = {
17 | val f = Future(action)
18 | f.onFailure {
19 | case e =>
20 | // HACK: Android does not close app when crash occurs in background thread, and there's no
21 | // cross-platform way to execute on the foreground thread.
22 | // We use this to make sure exceptions are not hidden in the logs.
23 | logger.error("Exception in Future", e)
24 | //System.exit(-1)
25 | }
26 | f
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/util/SeqNumGenerator.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import com.nutomic.ensichat.core.interfaces.SettingsInterface
4 | import com.nutomic.ensichat.core.messages.header.ContentHeader
5 |
6 | /**
7 | * Generates sequence numbers according to protocol, which are stored persistently.
8 | */
9 | final private[core] class SeqNumGenerator(preferences: SettingsInterface) {
10 |
11 | private val KeySequenceNumber = "sequence_number"
12 |
13 | private var current = preferences.get(KeySequenceNumber, ContentHeader.SeqNumRange.head)
14 |
15 | def next(): Int = {
16 | current += 1
17 | preferences.put(KeySequenceNumber, current)
18 | current
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/nutomic/ensichat/core/util/User.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import com.nutomic.ensichat.core.routing.Address
4 |
5 | final case class User(address: Address, name: String, status: String)
6 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/MessageTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages
2 |
3 | import java.io.ByteArrayInputStream
4 |
5 | import com.nutomic.ensichat.core
6 | import com.nutomic.ensichat.core.messages
7 | import com.nutomic.ensichat.core.messages.body.{Text, ConnectionInfo, ConnectionInfoTest}
8 | import com.nutomic.ensichat.core.messages.header.ContentHeaderTest._
9 | import com.nutomic.ensichat.core.messages.header.MessageHeader
10 | import com.nutomic.ensichat.core.routing.AddressTest
11 | import com.nutomic.ensichat.core.util.CryptoTest
12 | import junit.framework.TestCase
13 | import org.junit.Assert._
14 | import com.nutomic.ensichat.core.messages.MessageTest._
15 |
16 | import scala.collection.immutable.TreeSet
17 |
18 | object MessageTest {
19 |
20 | val m1 = new core.messages.Message(h1, new Text("first"))
21 |
22 | val m2 = new core.messages.Message(h2, new Text("second"))
23 |
24 | val m3 = new core.messages.Message(h3, new Text("third"))
25 |
26 | val messages = Set(m1, m2, m3)
27 |
28 | }
29 |
30 | class MessageTest extends TestCase {
31 |
32 | private lazy val crypto = CryptoTest.getCrypto
33 |
34 | def testOrder(): Unit = {
35 | var messages = new TreeSet[Message]()(core.messages.Message.Ordering)
36 | messages += m1
37 | messages += m2
38 | assertEquals(m1, messages.firstKey)
39 |
40 | messages = new TreeSet[Message]()(core.messages.Message.Ordering)
41 | messages += m2
42 | messages += m3
43 | assertEquals(m2, messages.firstKey)
44 | }
45 |
46 | def testSerializeSigned(): Unit = {
47 | val header = new MessageHeader(ConnectionInfo.Type, AddressTest.a4, AddressTest.a2, 0, 3)
48 | val m = new Message(header, ConnectionInfoTest.generateCi())
49 |
50 | val signed = crypto.sign(m)
51 | val bytes = signed.write
52 | val read = Message.read(new ByteArrayInputStream(bytes))
53 |
54 | assertEquals(signed, read)
55 | assertTrue(crypto.verify(read, Option(crypto.getLocalPublicKey)))
56 | }
57 |
58 | def testSerializeEncrypted(): Unit = {
59 | MessageTest.messages.foreach{ m =>
60 | val encrypted = crypto.encryptAndSign(m, Option(crypto.getLocalPublicKey))
61 | val bytes = encrypted.write
62 |
63 | val read = Message.read(new ByteArrayInputStream(bytes))
64 | assertEquals(encrypted.crypto, read.crypto)
65 | assertTrue(crypto.verify(read, Option(crypto.getLocalPublicKey)))
66 | val decrypted = crypto.decrypt(read)
67 | assertEquals(m.header, decrypted.header)
68 | assertEquals(m.body, decrypted.body)
69 | }
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/body/ConnectionInfoTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import com.nutomic.ensichat.core.messages.body
4 | import com.nutomic.ensichat.core.util.CryptoTest
5 | import junit.framework.TestCase
6 | import org.junit.Assert._
7 |
8 | object ConnectionInfoTest {
9 |
10 | def generateCi() = {
11 | val crypto = CryptoTest.getCrypto
12 | if (!crypto.localKeysExist)
13 | crypto.generateLocalKeys()
14 | new body.ConnectionInfo(crypto.getLocalPublicKey)
15 | }
16 |
17 | }
18 |
19 | class ConnectionInfoTest extends TestCase {
20 |
21 | def testWriteRead(): Unit = {
22 | val ci = ConnectionInfoTest.generateCi()
23 | val bytes = ci.write
24 | val body = ConnectionInfo.read(bytes)
25 | assertEquals(ci.key, body.asInstanceOf[ConnectionInfo].key)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/body/RouteErrorTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import com.nutomic.ensichat.core.routing.AddressTest
4 | import junit.framework.TestCase
5 | import org.junit.Assert._
6 |
7 | class RouteErrorTest extends TestCase {
8 |
9 | def testWriteRead(): Unit = {
10 | val rerr = new RouteError(AddressTest.a2, 62000)
11 | val bytes = rerr.write
12 | val parsed = RouteError.read(bytes)
13 | assertEquals(rerr, parsed)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/body/RouteReplyTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import junit.framework.TestCase
4 | import org.junit.Assert._
5 |
6 | class RouteReplyTest extends TestCase {
7 |
8 | def testWriteRead(): Unit = {
9 | val rrep = new RouteReply(61000, 123)
10 | val bytes = rrep.write
11 | val parsed = RouteReply.read(bytes)
12 | assertEquals(rrep, parsed)
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/body/RouteRequestTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import com.nutomic.ensichat.core.routing.AddressTest
4 | import junit.framework.TestCase
5 | import org.junit.Assert._
6 |
7 | class RouteRequestTest extends TestCase {
8 |
9 | def testWriteRead(): Unit = {
10 | val rreq = new RouteRequest(AddressTest.a2, 60000, 60001, 60002)
11 | val bytes = rreq.write
12 | val parsed = RouteRequest.read(bytes)
13 | assertEquals(rreq, parsed)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/body/UserInfoTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.body
2 |
3 | import junit.framework.TestCase
4 | import org.junit.Assert._
5 |
6 | class UserInfoTest extends TestCase {
7 |
8 | def testWriteRead(): Unit = {
9 | val name = new UserInfo("name", "status")
10 | val bytes = name.write
11 | val body = UserInfo.read(bytes)
12 | assertEquals(name, body.asInstanceOf[UserInfo])
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/header/ContentHeaderTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.header
2 |
3 | import java.util.GregorianCalendar
4 |
5 | import com.nutomic.ensichat.core.messages
6 | import com.nutomic.ensichat.core.messages.body.Text
7 | import com.nutomic.ensichat.core.messages.header
8 | import com.nutomic.ensichat.core.routing.{Address, AddressTest}
9 | import junit.framework.TestCase
10 | import org.joda.time.DateTime
11 | import org.junit.Assert._
12 |
13 | object ContentHeaderTest {
14 |
15 | val h1 = new header.ContentHeader(AddressTest.a1, AddressTest.a2, 1234,
16 | Text.Type, Some(123), Some(new DateTime(new GregorianCalendar(1970, 1, 1).getTime)), 3)
17 |
18 | val h2 = new header.ContentHeader(AddressTest.a1, AddressTest.a3,
19 | 30000, Text.Type, Some(8765), Some(new DateTime(new GregorianCalendar(2014, 6, 10))), 2)
20 |
21 | val h3 = new header.ContentHeader(AddressTest.a4, AddressTest.a2,
22 | 250, Text.Type, Some(77), Some(new DateTime(new GregorianCalendar(2020, 11, 11).getTime)), 1)
23 |
24 | val h4 = new header.ContentHeader(Address.Null, Address.Broadcast,
25 | header.ContentHeader.SeqNumRange.last, 0, Some(0xffff), Some(new DateTime(0L)), 6)
26 |
27 | val h5 = new header.ContentHeader(Address.Broadcast, Address.Null,
28 | 0, 0xff, Some(0), Some(new DateTime(0xffffffffL)), 0)
29 |
30 | val headers = Set(h1, h2, h3, h4, h5)
31 |
32 | }
33 |
34 | class ContentHeaderTest extends TestCase {
35 |
36 | def testSerialize(): Unit = {
37 | ContentHeaderTest.headers.foreach{h =>
38 | val bytes = h.write(0)
39 | assertEquals(bytes.length, h.length)
40 | val (mh, length) = MessageHeader.read(bytes)
41 | val chBytes = bytes.drop(mh.length)
42 | val (header, remaining) = messages.header.ContentHeader.read(mh, chBytes)
43 | assertEquals(h, header)
44 | assertEquals(0, remaining.length)
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/messages/header/MessageHeaderTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.messages.header
2 |
3 | import com.nutomic.ensichat.core.messages
4 | import com.nutomic.ensichat.core.messages.header
5 | import com.nutomic.ensichat.core.messages.header.MessageHeaderTest._
6 | import com.nutomic.ensichat.core.routing.{Address, AddressTest}
7 | import junit.framework.TestCase
8 | import org.junit.Assert._
9 |
10 | object MessageHeaderTest {
11 |
12 | val h1 = new header.MessageHeader(header.ContentHeader.ContentMessageType, AddressTest.a1, AddressTest.a2, 3,
13 | 0)
14 |
15 | val h2 = new header.MessageHeader(header.ContentHeader.ContentMessageType, Address.Null, Address.Broadcast,
16 | header.ContentHeader.SeqNumRange.last, 6, 3)
17 |
18 | val h3 = new header.MessageHeader(header.ContentHeader.ContentMessageType, Address.Broadcast, Address.Null, 0, 3)
19 |
20 | val headers = Set(h1, h2, h3)
21 |
22 | }
23 |
24 | class MessageHeaderTest extends TestCase {
25 |
26 | def testSerialize(): Unit = {
27 | headers.foreach{h =>
28 | val bytes = h.write(0)
29 | val (header, length) = messages.header.MessageHeader.read(bytes)
30 | assertEquals(h, header)
31 | assertEquals(messages.header.MessageHeader.Length, length)
32 | }
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/routing/AddressTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import com.nutomic.ensichat.core.routing.AddressTest._
4 | import junit.framework.TestCase
5 | import org.junit.Assert._
6 |
7 | object AddressTest {
8 |
9 | val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
10 |
11 | val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
12 |
13 | val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
14 |
15 | val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
16 |
17 | val a1Dashed =
18 | new Address("A51B7447-5EE622C3-C924DB14-7668F85E-024CA0B4-4CA146B5-E3D3C31A-54B34C1E")
19 |
20 | val Addresses = Set(a1, a1Dashed, a2, a3, a4, Address.Broadcast, Address.Null)
21 |
22 | val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104,
23 | -8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte)
24 |
25 | }
26 |
27 | class AddressTest extends TestCase {
28 |
29 | def testEncode(): Unit = {
30 | Addresses.foreach{a =>
31 | val base32 = a.toString
32 | val read = new Address(base32)
33 | assertEquals(a, read)
34 | assertEquals(a.hashCode, read.hashCode)
35 | }
36 |
37 | assertEquals(a1, new Address(a1Binary))
38 | assertEquals(a1Binary.deep, a1.bytes.deep)
39 | }
40 |
41 | def testDashes(): Unit = {
42 | assertEquals(a1, a1Dashed)
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/routing/LocalRoutesInfoTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import com.nutomic.ensichat.core.routing
4 | import junit.framework.TestCase
5 | import org.joda.time.{DateTime, DateTimeUtils, Duration}
6 | import org.junit.Assert._
7 |
8 | class LocalRoutesInfoTest extends TestCase {
9 |
10 | private def connections() = Set(AddressTest.a1, AddressTest.a2)
11 |
12 | def testRoute(): Unit = {
13 | val routesInfo = new routing.LocalRoutesInfo(connections)
14 | routesInfo.addRoute(AddressTest.a3, 0, AddressTest.a1, 1)
15 | val route = routesInfo.getRoute(AddressTest.a3)
16 | assertEquals(AddressTest.a1, route.get.nextHop)
17 | }
18 |
19 | def testBestMetric(): Unit = {
20 | val routesInfo = new routing.LocalRoutesInfo(connections)
21 | routesInfo.addRoute(AddressTest.a3, 0, AddressTest.a1, 1)
22 | routesInfo.addRoute(AddressTest.a3, 0, AddressTest.a2, 2)
23 | val route = routesInfo.getRoute(AddressTest.a3)
24 | assertEquals(AddressTest.a1, route.get.nextHop)
25 | }
26 |
27 | def testConnectionClosed(): Unit = {
28 | val routesInfo = new routing.LocalRoutesInfo(connections)
29 | routesInfo.addRoute(AddressTest.a3, 0, AddressTest.a1, 1)
30 | routesInfo.addRoute(AddressTest.a4, 0, AddressTest.a1, 1)
31 | // Mark the route as active, because only active routes are returned.
32 | routesInfo.getRoute(AddressTest.a3)
33 | val unreachable = routesInfo.connectionClosed(AddressTest.a1)
34 | assertEquals(Set(AddressTest.a3), unreachable)
35 | }
36 |
37 | def testTimeout(): Unit = {
38 | DateTimeUtils.setCurrentMillisFixed(new DateTime().getMillis)
39 | val routesInfo = new routing.LocalRoutesInfo(connections)
40 | routesInfo.addRoute(AddressTest.a3, 0, AddressTest.a1, 1)
41 | DateTimeUtils.setCurrentMillisFixed(DateTime.now.plus(Duration.standardSeconds(400)).getMillis)
42 | assertEquals(None, routesInfo.getRoute(AddressTest.a3))
43 | }
44 |
45 | def testNeighbor(): Unit = {
46 | val routesInfo = new routing.LocalRoutesInfo(connections)
47 | val r1 = routesInfo.getRoute(AddressTest.a1)
48 | assertTrue(r1.isDefined)
49 | assertEquals(AddressTest.a1, r1.get.destination)
50 | assertEquals(1, r1.get.metric)
51 | }
52 |
53 | def testGetAllAvailableRoutes(): Unit = {
54 | val routesInfo = new routing.LocalRoutesInfo(connections)
55 | routesInfo.addRoute(AddressTest.a3, 0, AddressTest.a1, 1)
56 | val destinations = routesInfo.getAllAvailableRoutes.map(_.destination).toSet
57 | assertEquals(connections() + AddressTest.a3, destinations)
58 |
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/routing/MessageBufferTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import java.util.concurrent.{CountDownLatch, TimeUnit}
4 |
5 | import com.nutomic.ensichat.core._
6 | import com.nutomic.ensichat.core.messages.MessageTest
7 | import com.nutomic.ensichat.core.messages.header.ContentHeader
8 | import junit.framework.TestCase
9 | import org.joda.time.DateTime
10 | import org.junit.Assert._
11 |
12 | class MessageBufferTest extends TestCase {
13 |
14 | /**
15 | * MessageBuffer checks the time of a message, we have to use the current time or items might
16 | * time out.
17 | */
18 | private def adjustMessageTime(m: messages.Message) =
19 | new messages.Message(m.header.asInstanceOf[ContentHeader].copy(time=Some(DateTime.now)), m.body)
20 |
21 | val m1 = adjustMessageTime(MessageTest.m1)
22 | val m2 = adjustMessageTime(MessageTest.m2)
23 |
24 | def testGetMessages(): Unit = {
25 | val buffer = new routing.MessageBuffer(routing.Address.Null, () => _)
26 | buffer.addMessage(m1)
27 | buffer.addMessage(m2)
28 | val msgs = buffer.getMessages(m1.header.target)
29 | assertEquals(1, msgs.size)
30 | assertEquals(m1, msgs.head)
31 | }
32 |
33 | def testRetryMessage(): Unit = {
34 | val latch = new CountDownLatch(1)
35 | val buffer = new routing.MessageBuffer(routing.Address.Null, { e =>
36 | assertEquals(m1.header.target, e)
37 | latch.countDown()
38 | })
39 | buffer.addMessage(m1)
40 | assertTrue(latch.await(15, TimeUnit.SECONDS))
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/routing/RouteMessageInfoTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import com.nutomic.ensichat.core.messages.Message
4 | import com.nutomic.ensichat.core.messages.body.{RouteReply, RouteRequest}
5 | import com.nutomic.ensichat.core.messages.header.MessageHeader
6 | import com.nutomic.ensichat.core.routing
7 | import junit.framework.TestCase
8 | import org.joda.time.{DateTime, DateTimeUtils, Duration}
9 | import org.junit.Assert._
10 |
11 | class RouteMessageInfoTest extends TestCase {
12 |
13 | /**
14 | * Test case in which we have an entry with the same type, origin and target.
15 | */
16 | def testSameMessage(): Unit = {
17 | val header = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
18 | val msg = new Message(header, new RouteRequest(AddressTest.a3, 2, 3, 1))
19 | val rmi = new routing.RouteMessageInfo()
20 | assertFalse(rmi.isMessageRedundant(msg))
21 | assertTrue(rmi.isMessageRedundant(msg))
22 | }
23 |
24 | /**
25 | * Forward a message with a seqnum that is older than the latest.
26 | */
27 | def testSeqNumOlder(): Unit = {
28 | val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
29 | val msg1 = new Message(header1, new RouteRequest(AddressTest.a3, 0, 0, 0))
30 | val rmi = new routing.RouteMessageInfo()
31 | assertFalse(rmi.isMessageRedundant(msg1))
32 |
33 | val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 3, 0)
34 | val msg2 = new Message(header2, new RouteRequest(AddressTest.a3, 2, 0, 0))
35 | assertTrue(rmi.isMessageRedundant(msg2))
36 | }
37 |
38 | /**
39 | * Announce a route with a metric that is worse than the existing one.
40 | */
41 | def testMetricWorse(): Unit = {
42 | val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
43 | val msg1 = new Message(header1, new RouteRequest(AddressTest.a3, 1, 0, 2))
44 | val rmi = new routing.RouteMessageInfo()
45 | assertFalse(rmi.isMessageRedundant(msg1))
46 |
47 | val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 2, 0)
48 | val msg2 = new Message(header2, new RouteRequest(AddressTest.a3, 1, 0, 4))
49 | assertTrue(rmi.isMessageRedundant(msg2))
50 | }
51 |
52 | /**
53 | * Announce route with a better metric.
54 | */
55 | def testMetricBetter(): Unit = {
56 | val header1 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
57 | val msg1 = new Message(header1, new RouteReply(0, 4))
58 | val rmi = new routing.RouteMessageInfo()
59 | assertFalse(rmi.isMessageRedundant(msg1))
60 |
61 | val header2 = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 2, 0)
62 | val msg2 = new Message(header2, new RouteReply(0, 2))
63 | assertFalse(rmi.isMessageRedundant(msg2))
64 | }
65 |
66 | /**
67 | * Test that entries are removed after [[RouteMessageInfo.MaxSeqnumLifetime]].
68 | */
69 | def testTimeout(): Unit = {
70 | val rmi = new routing.RouteMessageInfo()
71 | DateTimeUtils.setCurrentMillisFixed(DateTime.now.getMillis)
72 | val header = new MessageHeader(RouteRequest.Type, AddressTest.a1, AddressTest.a2, 1, 0)
73 | val msg = new Message(header, new RouteRequest(AddressTest.a3, 0, 0, 0))
74 | assertFalse(rmi.isMessageRedundant(msg))
75 |
76 | DateTimeUtils.setCurrentMillisFixed(DateTime.now.plus(Duration.standardSeconds(400)).getMillis)
77 | assertFalse(rmi.isMessageRedundant(msg))
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/routing/RouterTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.routing
2 |
3 | import java.util.GregorianCalendar
4 | import java.util.concurrent.{CountDownLatch, TimeUnit}
5 |
6 | import com.nutomic.ensichat.core.messages.body.{Text, UserInfo}
7 | import com.nutomic.ensichat.core.messages.header.ContentHeader
8 | import com.nutomic.ensichat.core.{messages, routing}
9 | import junit.framework.TestCase
10 | import org.joda.time.DateTime
11 | import org.junit.Assert._
12 |
13 | class RouterTest extends TestCase {
14 |
15 | private def neighbors() = Set[routing.Address](AddressTest.a1, AddressTest.a2, AddressTest.a4)
16 |
17 | def testNoRouteFound(): Unit = {
18 | val msg = generateMessage(AddressTest.a2, AddressTest.a3, 1)
19 | val latch = new CountDownLatch(1)
20 | val router = new routing.Router(new LocalRoutesInfo(neighbors),
21 | (_, _) => fail("Message shouldn't be forwarded"), m => {
22 | assertEquals(msg, m)
23 | latch.countDown()
24 | })
25 | router.forwardMessage(msg)
26 | assertTrue(latch.await(1, TimeUnit.SECONDS))
27 | }
28 |
29 | def testNextHop(): Unit = {
30 | val msg = generateMessage(AddressTest.a1, AddressTest.a4, 1)
31 | var sentTo = Set[routing.Address]()
32 | val router = new routing.Router(new LocalRoutesInfo(neighbors),
33 | (a, m) => {
34 | sentTo += a
35 | }, _ => ())
36 |
37 | router.forwardMessage(msg)
38 | assertEquals(Set(AddressTest.a4), sentTo)
39 | }
40 |
41 | def testMessageSame(): Unit = {
42 | val msg = generateMessage(AddressTest.a1, AddressTest.a4, 1)
43 | val router = new routing.Router(new LocalRoutesInfo(neighbors),
44 | (a, m) => {
45 | assertEquals(msg.header.origin, m.header.origin)
46 | assertEquals(msg.header.target, m.header.target)
47 | assertEquals(msg.header.seqNum, m.header.seqNum)
48 | assertEquals(msg.header.protocolType, m.header.protocolType)
49 | assertEquals(msg.header.hopCount + 1, m.header.hopCount)
50 | assertEquals(msg.header.tokens, m.header.tokens)
51 | assertEquals(msg.body, m.body)
52 | assertEquals(msg.crypto, m.crypto)
53 | }, _ => ())
54 | router.forwardMessage(msg)
55 | }
56 |
57 | /**
58 | * Messages from different senders with the same sequence number should be forwarded.
59 | */
60 | def testDifferentSenders(): Unit = {
61 | var sentTo = Set[routing.Address]()
62 | val router = new routing.Router(new LocalRoutesInfo(neighbors), (a, m) => sentTo += a, _ => ())
63 |
64 | router.forwardMessage(generateMessage(AddressTest.a1, AddressTest.a4, 1))
65 | assertEquals(Set(AddressTest.a4), sentTo)
66 |
67 | sentTo = Set[routing.Address]()
68 | router.forwardMessage(generateMessage(AddressTest.a2, AddressTest.a4, 1))
69 | assertEquals(Set(AddressTest.a4), sentTo)
70 | }
71 |
72 | def testSeqNumComparison(): Unit = {
73 | routing.Router.compare(1, ContentHeader.SeqNumRange.last)
74 | routing.Router.compare(ContentHeader.SeqNumRange.last / 2, ContentHeader.SeqNumRange.last)
75 | routing.Router.compare(ContentHeader.SeqNumRange.last / 2, 1)
76 | }
77 |
78 | def testDiscardOldIgnores(): Unit = {
79 | def test(first: Int, second: Int) {
80 | var sentTo = Set[routing.Address]()
81 | val router = new routing.Router(new LocalRoutesInfo(neighbors), (a, m) => sentTo += a, _ => ())
82 | router.forwardMessage(generateMessage(AddressTest.a1, AddressTest.a4, first))
83 | router.forwardMessage(generateMessage(AddressTest.a1, AddressTest.a4, second))
84 |
85 | sentTo = Set[routing.Address]()
86 | router.forwardMessage(generateMessage(AddressTest.a1, AddressTest.a4, first))
87 | assertEquals(Set(AddressTest.a4), sentTo)
88 | }
89 |
90 | test(1, ContentHeader.SeqNumRange.last)
91 | test(ContentHeader.SeqNumRange.last / 2, ContentHeader.SeqNumRange.last)
92 | test(ContentHeader.SeqNumRange.last / 2, 1)
93 | }
94 |
95 | def testHopLimit(): Unit = Range(19, 22).foreach { i =>
96 | val msg = new messages.Message(
97 | new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(DateTime.now), 3, i), new Text(""))
98 | val router = new routing.Router(new LocalRoutesInfo(neighbors), (a, m) => fail(), _ => ())
99 | router.forwardMessage(msg)
100 | }
101 |
102 | private def generateMessage(sender: routing.Address, receiver: routing.Address, seqNum: Int): messages.Message = {
103 | val header = new ContentHeader(sender, receiver, seqNum, UserInfo.Type, Some(5),
104 | Some(new DateTime(new GregorianCalendar(2014, 6, 10).getTime)), 3)
105 | new messages.Message(header, new UserInfo("", ""))
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/util/CryptoTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import java.io.File
4 |
5 | import com.nutomic.ensichat.core.interfaces.SettingsInterface
6 | import com.nutomic.ensichat.core.messages.MessageTest
7 | import com.nutomic.ensichat.core.util
8 | import junit.framework.TestCase
9 | import org.junit.Assert._
10 |
11 | object CryptoTest {
12 |
13 | class TestSettings extends SettingsInterface {
14 | private var map = Map[String, Any]()
15 | override def get[T](key: String, default: T): T = map.getOrElse(key, default).asInstanceOf[T]
16 | override def put[T](key: String, value: T): Unit = map += (key -> value)
17 | }
18 |
19 | def getCrypto: util.Crypto = {
20 | val tempFolder = new File(System.getProperty("testDir"), "/crypto/")
21 | val crypto = new util.Crypto(new TestSettings(), tempFolder)
22 | if (!crypto.localKeysExist) {
23 | crypto.generateLocalKeys()
24 | }
25 | crypto
26 | }
27 |
28 | }
29 |
30 | class CryptoTest extends TestCase {
31 |
32 | private lazy val crypto = CryptoTest.getCrypto
33 |
34 | def testSignVerify(): Unit = {
35 | MessageTest.messages.foreach { m =>
36 | val signed = crypto.sign(m)
37 | assertTrue(crypto.verify(signed, Option(crypto.getLocalPublicKey)))
38 | assertEquals(m.header, signed.header)
39 | assertEquals(m.body, signed.body)
40 | }
41 | }
42 |
43 | def testEncryptDecrypt(): Unit = {
44 | MessageTest.messages.foreach{ m =>
45 | val encrypted = crypto.encryptAndSign(m, Option(crypto.getLocalPublicKey))
46 | assertTrue(crypto.verify(encrypted, Option(crypto.getLocalPublicKey)))
47 | val decrypted = crypto.decrypt(encrypted)
48 | assertEquals(m.body, decrypted.body)
49 | assertEquals(m.header, encrypted.header)
50 | }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/util/DatabaseTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import java.io.File
4 | import java.util.GregorianCalendar
5 | import java.util.concurrent.CountDownLatch
6 |
7 | import com.nutomic.ensichat.core.interfaces.{CallbackInterface, SettingsInterface}
8 | import com.nutomic.ensichat.core.messages.Message
9 | import com.nutomic.ensichat.core.messages.body.Text
10 | import com.nutomic.ensichat.core.messages.header.ContentHeader
11 | import com.nutomic.ensichat.core.routing.Address
12 | import com.nutomic.ensichat.core.util.DatabaseTest._
13 | import junit.framework.Assert._
14 | import junit.framework.TestCase
15 | import org.joda.time.DateTime
16 |
17 | object DatabaseTest {
18 |
19 | private val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
20 | private val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
21 | private val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
22 | private val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
23 |
24 | private val h1 = new ContentHeader(a2, a1, -1, Text.Type, Some(123),
25 | Some(new DateTime(new GregorianCalendar(1970, 1, 1).getTime)), 0)
26 | private val h2 = new ContentHeader(a1, a3, -1, Text.Type, Some(8765),
27 | Some(new DateTime(new GregorianCalendar(2014, 6, 10).getTime)), 0)
28 | private val h3 = new ContentHeader(a4, a2, -1, Text.Type, Some(77),
29 | Some(new DateTime(new GregorianCalendar(2020, 11, 11).getTime)), 0)
30 |
31 | private val m1 = new Message(h1, new Text("first"))
32 | private val m2 = new Message(h2, new Text("second"))
33 | private val m3 = new Message(h3, new Text("third"))
34 |
35 | private val u1 = new User(a1, "one", "s1")
36 | private val u2 = new User(a2, "two", "s2")
37 | private val u3 = new User(a2, "two-updated", "s2-updated")
38 |
39 | }
40 |
41 | class DatabaseTest extends TestCase {
42 |
43 | private val databaseFile = File.createTempFile("ensichat-test", ".db")
44 |
45 | private val latch = new CountDownLatch(1)
46 |
47 | private val database = new Database(databaseFile, new SettingsInterface {
48 | private var values = Map[String, Any]()
49 | override def get[T](key: String, default: T): T = values.getOrElse(key, default).asInstanceOf[T]
50 | override def put[T](key: String, value: T): Unit = values += (key -> value)
51 | }, new CallbackInterface {
52 | override def onConnectionsChanged(): Unit = {}
53 | override def onContactsUpdated(): Unit = {
54 | latch.countDown()
55 | }
56 | override def onMessageReceived(msg: Message): Unit = {}
57 | })
58 |
59 | override def tearDown(): Unit = {
60 | super.tearDown()
61 | database.close()
62 | databaseFile.delete()
63 | }
64 |
65 | def testMessageSelect(): Unit = {
66 | database.onMessageReceived(m1)
67 | database.onMessageReceived(m2)
68 | database.onMessageReceived(m3)
69 | val msg = database.getMessages(a2)
70 | assertEquals(Seq(m1, m3), msg)
71 | }
72 |
73 | def testAddContact(): Unit = {
74 | assertEquals(0, database.getContacts.size)
75 | database.addContact(u1)
76 | val contacts = database.getContacts
77 | assertEquals(1, contacts.size)
78 | assertEquals(Option(u1), database.getContact(u1.address))
79 | }
80 |
81 | def testAddContactCallback(): Unit = {
82 | database.addContact(u1)
83 | latch.await()
84 | }
85 |
86 | def testGetContact(): Unit = {
87 | assertFalse(database.getContact(u2.address).isDefined)
88 | database.addContact(u2)
89 | val c = database.getContact(u2.address)
90 | assertEquals(u2, c.get)
91 | }
92 |
93 | def testUpdateContact(): Unit = {
94 | database.addContact(u2)
95 | database.updateContact(u3)
96 | val c = database.getContact(u2.address)
97 | assertEquals(u3, c.get)
98 | }
99 |
100 | def testUpdateNonExistingContact(): Unit = {
101 | try {
102 | database.updateContact(u3)
103 | fail()
104 | } catch {
105 | case _: AssertionError =>
106 | }
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/nutomic/ensichat/core/util/UserTest.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.core.util
2 |
3 | import com.nutomic.ensichat.core.routing.AddressTest
4 | import com.nutomic.ensichat.core.util
5 |
6 | object UserTest {
7 |
8 | val u1 = new util.User(AddressTest.a1, "one", "s1")
9 |
10 | val u2 = new util.User(AddressTest.a2, "two", "s2")
11 |
12 | val u3 = new util.User(AddressTest.a3, "three", "s3")
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/docs/bachelor-thesis.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/docs/bachelor-thesis.pdf
--------------------------------------------------------------------------------
/gradle/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/gradle/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Dec 04 22:57:09 EET 2014
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip
7 |
--------------------------------------------------------------------------------
/gradle/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/gradle/local.properties:
--------------------------------------------------------------------------------
1 | ## This file is automatically generated by Android Studio.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 | #
7 | # Location of the SDK. This is only used by Gradle.
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | #Thu Dec 04 22:57:09 EET 2014
11 | sdk.dir=/home/felix/software/android-sdk
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jan 23 18:44:50 CET 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
7 | distributionSha256Sum=88a910cdf2e03ebbb5fe90f7ecf534fc9ac22e12112dc9a2fee810c598a76091
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/graphics/ic_launcher.svg:
--------------------------------------------------------------------------------
1 |
2 |
146 |
--------------------------------------------------------------------------------
/graphics/screenshot_phone_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/graphics/screenshot_phone_1.png
--------------------------------------------------------------------------------
/graphics/screenshot_phone_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/graphics/screenshot_phone_2.png
--------------------------------------------------------------------------------
/graphics/screenshot_phone_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nutomic/ensichat/b9eddea5b903414af8c5ce38dd49d43d7a98a6a0/graphics/screenshot_phone_3.png
--------------------------------------------------------------------------------
/integration/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/integration/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'scala'
2 | apply plugin: 'application'
3 |
4 | dependencies {
5 | compile 'org.scala-lang:scala-library:2.11.7'
6 | compile 'com.github.scala-incubator.io:scala-io-file_2.11:0.4.3'
7 | compile project(path: ':core')
8 | }
9 |
10 | mainClassName = 'com.nutomic.ensichat.integration.Main'
11 | applicationName = 'ensichat-server'
12 |
--------------------------------------------------------------------------------
/integration/src/main/scala/com.nutomic.ensichat.integration/LocalNode.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.integration
2 |
3 | import java.io.File
4 | import java.util.concurrent.LinkedBlockingQueue
5 |
6 | import com.nutomic.ensichat.core.interfaces.{CallbackInterface, SettingsInterface}
7 | import com.nutomic.ensichat.core.messages.Message
8 | import com.nutomic.ensichat.core.util.{Crypto, Database}
9 | import com.nutomic.ensichat.core.ConnectionHandler
10 | import com.nutomic.ensichat.integration.LocalNode._
11 |
12 | import scala.concurrent.Await
13 | import scala.concurrent.duration.Duration
14 | import scalax.file.Path
15 |
16 | object LocalNode {
17 |
18 | private final val StartingPort = 21000
19 |
20 | object EventType extends Enumeration {
21 | type EventType = Value
22 | val MessageReceived, ConnectionsChanged, ContactsUpdated = Value
23 | }
24 |
25 | class FifoStream[A]() {
26 | private val queue = new LinkedBlockingQueue[Option[A]]()
27 | def toStream: Stream[A] = queue.take match {
28 | case Some(a) => Stream.cons(a, toStream)
29 | case None => Stream.empty
30 | }
31 | def close() = queue add None
32 | def enqueue(a: A) = queue.put(Option(a))
33 | }
34 |
35 | }
36 |
37 | /**
38 | * Runs an ensichat node on localhost.
39 | *
40 | * Received messages can be accessed through [[eventQueue]].
41 | *
42 | * @param index Number of this node. The server port is opened on port [[StartingPort]] + index.
43 | * @param configFolder Folder where keys and configuration should be stored.
44 | */
45 | class LocalNode(val index: Int, configFolder: File) extends CallbackInterface {
46 | private val databaseFile = new File(configFolder, "database")
47 | private val keyFolder = new File(configFolder, "keys")
48 |
49 | private val settings = new SettingsInterface {
50 | private var values = Map[String, Any]()
51 | override def get[T](key: String, default: T): T = values.get(key).map(_.asInstanceOf[T]).getOrElse(default)
52 | override def put[T](key: String, value: T): Unit = values += (key -> value.asInstanceOf[Any])
53 | }
54 |
55 | val crypto = new Crypto(settings, keyFolder)
56 | val database = new Database(databaseFile, settings, this)
57 | val connectionHandler = new ConnectionHandler(settings, database, this, crypto, port)
58 | val eventQueue = new FifoStream[(EventType.EventType, Option[Message])]()
59 |
60 | configFolder.mkdirs()
61 | keyFolder.mkdirs()
62 | settings.put(SettingsInterface.KeyAddresses, "")
63 | Await.result(connectionHandler.start(), Duration.Inf)
64 |
65 | def port = StartingPort + index
66 |
67 | def stop(): Unit = {
68 | connectionHandler.stop()
69 | Path(configFolder).deleteRecursively()
70 | }
71 |
72 | def onMessageReceived(msg: Message): Unit = {
73 | eventQueue.enqueue((EventType.MessageReceived, Option(msg)))
74 | }
75 |
76 | def onConnectionsChanged(): Unit =
77 | eventQueue.enqueue((EventType.ConnectionsChanged, None))
78 |
79 | def onContactsUpdated(): Unit =
80 | eventQueue.enqueue((EventType.ContactsUpdated, None))
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/server/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'scala'
2 | apply plugin: 'application'
3 |
4 | dependencies {
5 | compile 'org.scala-lang:scala-library:2.11.7'
6 | compile project(path: ':core')
7 | compile 'com.github.scopt:scopt_2.10:3.3.0'
8 | compile 'ch.qos.logback:logback-classic:1.1.7'
9 | }
10 |
11 | mainClassName = 'com.nutomic.ensichat.server.Main'
12 | version = "0.5.2"
13 | applicationName = 'ensichat-server'
14 |
15 | jar {
16 | archiveName "${applicationName}.jar"
17 | manifest.attributes 'Main-Class': mainClassName
18 | from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) })
19 | }
20 |
21 | task release(type: Zip) {
22 | archiveName = "$applicationName-${version}.zip"
23 | def baseDir = archiveName - '.zip'
24 | into(baseDir) {
25 | from(project.file('src/dist'))
26 | into ('lib') { from ('build/libs/') }
27 | }
28 | }
29 | release.dependsOn(jar)
30 |
31 | run {
32 | // Use this to pass command line arguments via `gradle run`.
33 | //
34 | // Uses comma instead of space for command seperation for simpler parsing.
35 | //
36 | // Examples:
37 | // ```
38 | // ./gradlew server:run -Pargs="--help"
39 | // ./gradlew server:run -Pargs="--name,MyName"
40 | // ./gradlew server:run -Pargs="--name,MyName,--status,My Status"
41 | // ```
42 | if (project.hasProperty('args')) {
43 | args project.args.split('\\s+')
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/src/dist/etc/linux-systemd/ensichat.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Ensichat Server
3 | After=network.target
4 |
5 | [Service]
6 | User=ensichat
7 | Group=ensichat
8 | ExecStart=/usr/bin/java -jar /usr/lib/ensichat-server.jar --name "unknown user" --status ""
9 | WorkingDirectory=/var/lib/ensichat
10 | RootDirectory=/var/lib/ensichat
11 |
12 | Restart=always
13 | PrivateTmp=true
14 | ProtectSystem=full
15 | ProtectHome=true
16 | NoNewPrivileges=true
17 |
18 | [Install]
19 | WantedBy=multi-user.target
20 |
--------------------------------------------------------------------------------
/server/src/main/scala/com/nutomic/ensichat/server/Config.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.server
2 |
3 | case class Config(name: Option[String] = None, status: Option[String] = None)
4 |
--------------------------------------------------------------------------------
/server/src/main/scala/com/nutomic/ensichat/server/Main.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.server
2 |
3 | import java.io.File
4 | import java.nio.file.Paths
5 | import java.util.concurrent.TimeUnit
6 |
7 | import com.nutomic.ensichat.core.messages.body.Text
8 | import com.nutomic.ensichat.core.interfaces.{CallbackInterface, SettingsInterface}
9 | import com.nutomic.ensichat.core.messages.Message
10 | import com.nutomic.ensichat.core.util.{Crypto, Database}
11 | import com.nutomic.ensichat.core.ConnectionHandler
12 | import com.typesafe.scalalogging.Logger
13 | import scopt.OptionParser
14 |
15 | object Main extends App with CallbackInterface {
16 |
17 | private val logger = Logger(this.getClass)
18 |
19 | private val ConfigFolder = Paths.get("").toFile.getAbsoluteFile
20 | private val ConfigFile = new File(ConfigFolder, "config.properties")
21 | private val DatabaseFile = new File(ConfigFolder, "database")
22 | private val KeyFolder = new File(ConfigFolder, "keys")
23 |
24 | private lazy val settings = new Settings(ConfigFile)
25 | private lazy val crypto = new Crypto(settings, KeyFolder)
26 | private lazy val database = new Database(DatabaseFile, settings, this)
27 | private lazy val connectionHandler = new ConnectionHandler(settings, database, this, crypto)
28 |
29 | init()
30 |
31 | /**
32 | * Initializes the app, parses command line parameters.
33 | *
34 | * See build.gradle for information about passing command line parameters from gradle.
35 | */
36 | private def init(): Unit = {
37 | ConfigFolder.mkdirs()
38 | KeyFolder.mkdirs()
39 | sys.addShutdownHook(connectionHandler.stop())
40 |
41 | val parser = new OptionParser[Config]("ensichat") {
42 | head("ensichat")
43 | opt[String]('n', "name") action { (x, c) =>
44 | c.copy(name = Option(x))
45 | } text "the username for this node (optional)"
46 | opt[String]('s', "status") action { (x, c) =>
47 | c.copy(status = Option(x))
48 | } text "the status line (optional)"
49 | help("help") text "prints this usage text"
50 | }
51 |
52 | parser.parse(args, Config()).foreach { config =>
53 | config.name.foreach(settings.put(SettingsInterface.KeyUserName, _))
54 | config.status.foreach(settings.put(SettingsInterface.KeyUserStatus, _))
55 | run()
56 | }
57 | }
58 |
59 | private def run(): Unit = {
60 | connectionHandler.start()
61 |
62 | // Keep alive and print logs
63 | while (true) {
64 | Thread.sleep(TimeUnit.SECONDS.toMillis(1))
65 | }
66 | }
67 |
68 | def onMessageReceived(msg: Message): Unit = {
69 | if (msg.header.target != crypto.localAddress)
70 | return
71 |
72 | msg.body match {
73 | case text: Text =>
74 | val address = msg.header.origin
75 | val name = connectionHandler.getUser(address).name
76 | connectionHandler.sendTo(address, new Text("Hello " + name))
77 | logger.info("Received text: " + text.text)
78 | case _ =>
79 | logger.info("Received msg: " + msg.body)
80 | }
81 | }
82 |
83 | def onConnectionsChanged(): Unit = {}
84 |
85 | def onContactsUpdated(): Unit = {}
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala:
--------------------------------------------------------------------------------
1 | package com.nutomic.ensichat.server
2 |
3 | import java.io._
4 | import java.util.Properties
5 |
6 | import com.nutomic.ensichat.core.interfaces.SettingsInterface
7 | import com.typesafe.scalalogging.Logger
8 |
9 | import scala.collection.JavaConverters._
10 |
11 | class Settings(file: File) extends SettingsInterface {
12 |
13 | private val logger = Logger(this.getClass)
14 |
15 | if (!file.exists()) {
16 | file.createNewFile()
17 | put(SettingsInterface.KeyUserName, "unknown user")
18 | }
19 |
20 | private lazy val props: Properties = {
21 | val p = new Properties()
22 | try {
23 | val fis = new InputStreamReader(new FileInputStream(file), "UTF-8")
24 | p.load(fis)
25 | fis.close()
26 | } catch {
27 | case e: IOException => logger.warn("Failed to load settings from " + file, e)
28 | }
29 | p
30 | }
31 |
32 | def put[T](key: String, value: T): Unit = {
33 | props.asScala.put(key, value.toString)
34 | try {
35 | val fos = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")
36 | props.store(fos, "")
37 | fos.close()
38 | } catch {
39 | case e: IOException => logger.warn("Failed to write preference for key " + key, e)
40 | }
41 | }
42 |
43 | def get[T](key: String, default: T): T = {
44 | val value = props.asScala.getOrElse[String](key, default.toString)
45 | val cast = default match {
46 | case _: Int => value.toInt
47 | case _: Long => value.toLong
48 | case _: String => value
49 | }
50 | // This has no effect due to type erasure, but is needed to avoid compiler error.
51 | cast.asInstanceOf[T]
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':android', ':core', ':server', ':integration'
2 |
--------------------------------------------------------------------------------