Compare commits
134 commits
send-bitco
...
master
Author | SHA1 | Date | |
---|---|---|---|
b9eddea5b9 | |||
e05e34e0fb | |||
de846891fc | |||
a4a50d0341 | |||
f50d0697e6 | |||
8334cd3926 | |||
2ed57f6dec | |||
e497c71fb0 | |||
fa2425b88c | |||
8f5d10f0c0 | |||
a22b32f607 | |||
75d87b54c2 | |||
858a2ac636 | |||
0ccfd100d3 | |||
f579608f6b | |||
a857002fa9 | |||
d1768ea8b4 | |||
6d65b739c6 | |||
ab5370cd86 | |||
68cb2253d9 | |||
e9da8cd1f5 | |||
c4ee46c038 | |||
81d8230abc | |||
083c0a2d03 | |||
1d7055f1d6 | |||
5bb97a1460 | |||
69066c6744 | |||
1d8736931f | |||
f3ec28fef8 | |||
127e4c9ff2 | |||
8bec10efb6 | |||
1be5ca31ab | |||
213046048a | |||
8f2d0cfb73 | |||
e886f1563a | |||
882b518a7c | |||
7c9c6803dc | |||
64fae3df99 | |||
6bebfcf3cf | |||
343c5fca07 | |||
acbcd68384 | |||
51b1feee4e | |||
579d1f5717 | |||
338a51fea9 | |||
c1c7487d48 | |||
6799deea9d | |||
989ec6efb1 | |||
45bb01cd8e | |||
5310c34218 | |||
4a36fdbef2 | |||
1add05b72f | |||
23ee0f6da7 | |||
a0cd4248d9 | |||
7ce2e937ab | |||
09ef3b3705 | |||
d97986ae81 | |||
83fc696cc7 | |||
2cc4928a99 | |||
8bafd62e35 | |||
7d1f929c2c | |||
453b2d7fe6 | |||
97e70c2092 | |||
b3212bf3f4 | |||
cfb0723c1f | |||
656f52d3f3 | |||
5d3720b7e9 | |||
127acfb3ab | |||
ca2c4c65ab | |||
07a2fde2af | |||
c148ee928c | |||
333b3495ae | |||
addf8e1950 | |||
4d6afbc9cd | |||
375d245765 | |||
f19fc3f6d1 | |||
721c99c6d9 | |||
3fb6c009ba | |||
6a8a7971f8 | |||
2c2bc1929d | |||
8c7d4db8c5 | |||
021e22e50b | |||
ba3ce67a06 | |||
02a506608f | |||
b58bc9d198 | |||
37d3ff4377 | |||
84e00a23d2 | |||
6e3cf1da63 | |||
9af22b8897 | |||
9bb70aa405 | |||
f3b956c89f | |||
9b19158adf | |||
ec6aeeb78f | |||
d2dab0121c | |||
0d56efc232 | |||
5e00a2341c | |||
6e12494c2c | |||
ddb3d64708 | |||
1df3d9a46f | |||
d2f497d4de | |||
6cf8a3c0b8 | |||
dc55d74f9b | |||
3cbba72117 | |||
d3c2b5ee26 | |||
244fd32762 | |||
c424bed315 | |||
ac12941ea8 | |||
39a2120300 | |||
514827ad8f | |||
1a8ae49f79 | |||
b21cf17cea | |||
199b185861 | |||
28cb4f15d9 | |||
6867b19380 | |||
1942986962 | |||
49293e259e | |||
925eb2d5c5 | |||
9b5175114f | |||
f5ee0996eb | |||
a951b05965 | |||
269fe41ebf | |||
8c84cb2924 | |||
67091caa5d | |||
|
7d1beeff24 | ||
86d9275dcf | |||
90069917b0 | |||
d64bf41813 | |||
10066b2c83 | |||
b11bf85d90 | |||
123c56c322 | |||
6042c3fb11 | |||
e66bccc044 | |||
87632b6225 | |||
cdcf0ace01 | |||
1c48484358 |
3
.gitignore
vendored
|
@ -4,4 +4,5 @@
|
|||
/build
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
*.ipr
|
||||
*.apk
|
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "buildSrc"]
|
||||
path = buildSrc
|
||||
url = https://github.com/xelnaga/gradle-android-scala-plugin.git
|
32
.travis.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
language: android
|
||||
jdk: oraclejdk8
|
||||
|
||||
# Install Android SDK
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-24.0.2
|
||||
- android-24
|
||||
- extra-android-m2repository
|
||||
|
||||
# Cache gradle dependencies
|
||||
# https://docs.travis-ci.com/user/languages/android/#Caching
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
|
||||
env:
|
||||
- GRADLE_OPTS=-Xmx2048m
|
||||
|
||||
script:
|
||||
# Lint fails because travis doesn't have platform-tools 24
|
||||
# https://github.com/travis-ci/travis-ci/issues/6699
|
||||
#- ./gradlew lint
|
||||
- ./gradlew core:test
|
||||
- ./gradlew server:release
|
||||
- ./gradlew integration:assemble
|
||||
- ./gradlew android:assembleRelRelease || ./gradlew android:assembleRelRelease
|
133
PROTOCOL.md
|
@ -26,6 +26,9 @@ Nodes MUST NOT have a public key with the broadcast address or null
|
|||
address as hash. Additionally, nodes MUST NOT connect to a node with
|
||||
either address.
|
||||
|
||||
All integer fields are in network byte order, and unsigned (unless
|
||||
specified otherwise).
|
||||
|
||||
|
||||
Crypto
|
||||
------
|
||||
|
@ -40,22 +43,15 @@ private key, and the result written to the 'Encryption Data' part.
|
|||
|
||||
Routing
|
||||
-------
|
||||
A simple flood routing protocol is currently used. Every node forwards
|
||||
all messages, unless a message with the same Origin and Sequence Number
|
||||
has already been received.
|
||||
The routing protocol is based on
|
||||
[AODVv2](https://datatracker.ietf.org/doc/draft-ietf-manet-aodvv2/),
|
||||
with various features left out.
|
||||
|
||||
Nodes MUST store pairs of (Origin, Sequence Number) for all received
|
||||
messages. After receiving a new message, entries with the same Origin
|
||||
and Sequence Number between _received_ + 1 and _received_ + 32767 MUST
|
||||
be removed (with a wrap around at the maximum value). The entries MUST
|
||||
NOT be cleared while the program is running. They MAY be cleared when
|
||||
the program is exited.
|
||||
TODO: Add Documentation for routing protocol.
|
||||
|
||||
There is currently no support for offline messages. If sender and
|
||||
receiver are not in the same mesh, the message will not arrive.
|
||||
|
||||
Nodes are free implement different routing algorithms.
|
||||
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
@ -84,14 +80,12 @@ AES key is wrapped with the recipient's public RSA key.
|
|||
### Header
|
||||
|
||||
Every message starts with one 74 byte header indicating the message
|
||||
version, type and ID, followed by the length of the message. The
|
||||
header is in network byte order, i.e. big endian. The header may have
|
||||
6 bytes of additional data.
|
||||
version, type and ID, followed by the length of the message.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Version | Protocol-Type | Hop Limit | Hop Count |
|
||||
| Version | Protocol-Type | Tokens | Hop Count |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
@ -117,8 +111,8 @@ where such a packet came from MAY be closed.
|
|||
Protocol-Type is one of those specified in section Protocol Messages,
|
||||
or 255 for Content Messages.
|
||||
|
||||
Hop Limit SHOULD be set to `MAX_HOP_COUNT` on message creation, and
|
||||
MUST NOT be changed by a forwarding node.
|
||||
Tokens is the number of times this message should be copied to
|
||||
different relays.
|
||||
|
||||
Hop Count specifies the number of nodes a message may pass. When
|
||||
creating a package, it is initialized to 0. Whenever a node forwards
|
||||
|
@ -220,6 +214,100 @@ After this message has been received, communication with normal messages
|
|||
may start.
|
||||
|
||||
|
||||
### Route Request (Protocol-Type = 2)
|
||||
|
||||
Sent to request a route to a specific Target Address.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
| Address (32 bytes) |
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| OrigSeqNum | OriginMetric |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| TargMetric |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Equivalent to the Sequence Number in the message header.
|
||||
|
||||
Set OrigMetric = RouterClient.Cost for the Router Client entry
|
||||
which includes OrigAddr.
|
||||
|
||||
If an Invalid route exists in the Local Route Set matching
|
||||
TargAddr using longest prefix matching and has a valid
|
||||
sequence number, set TargSeqNum = LocalRoute.SeqNum.
|
||||
Otherwise, set TargSeqNum = -1. This field is signed.
|
||||
|
||||
### Route Reply (Protocol-Type = 3)
|
||||
|
||||
Sent as a reply when a Route Request arrives, to inform other nodes
|
||||
about a route.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| TargSeqNum | TargMetric |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Set TargMetric = RouterClient.Cost for the Router Client entry
|
||||
which includes TargAddr.
|
||||
|
||||
### Route Error (Protocol-Type = 4)
|
||||
|
||||
Notifies other nodes of routes that are no longer available. The target
|
||||
address MUST be set to the broadcast address.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
| Packet Source (32 bytes) |
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
| Address (32 bytes) |
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| SeqNum |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Packet Source is the source address of the message triggering this
|
||||
Route Error. If the route error is not triggered by a message,
|
||||
this MUST be set to the null address.
|
||||
|
||||
Address is the address that is no longer reachable.
|
||||
|
||||
SeqNum is the sequence number of the route that is no longer available
|
||||
(if known). Otherwise, set TargSeqNum = -1. This field is signed.
|
||||
|
||||
### PublicKeyRequest (Protocol-Type = 5)
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Address |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Contains an address for which the sender wants the corresponding public
|
||||
key.
|
||||
|
||||
### PublicKeyReply (Protocol-Type = 6)
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Key Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Key (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Contains a node's public key in binary form. Sent in reply to a
|
||||
PublicKeyRequest.
|
||||
|
||||
Content Messages
|
||||
----------------
|
||||
|
||||
|
@ -265,3 +353,14 @@ Text the string to be transferred, encoded as UTF-8.
|
|||
|
||||
Contains the sender's name and status, which should be used for
|
||||
display to users.
|
||||
|
||||
### MessageReceived (Content-Type = 8)
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Message ID |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Confirms that a previous content message has been received by the
|
||||
target node. Message ID is the ID of that message.
|
||||
|
|
44
README.md
|
@ -1,11 +1,24 @@
|
|||
PROJECT DISCONTINUED
|
||||
====================
|
||||
|
||||
Unfortunately, I won't be able to continue development on Ensichat, due to lack of time. I suggest
|
||||
you give [Briar](https://briarproject.org/) a try instead.
|
||||
|
||||
If you wish to take over maintenance of the project, please contact me.
|
||||
|
||||
Ensichat
|
||||
========
|
||||
|
||||
[![BitCoin donate button](https://img.shields.io/badge/bitcoin-donate-yellow.svg)](https://blockchain.info/address/1DmU6QVGSKXGXJU1bqmmStPDNsNnYoMJB4)
|
||||
[![Build Status](https://travis-ci.org/Nutomic/ensichat.svg?branch=master)](https://travis-ci.org/Nutomic/ensichat)
|
||||
[![License: MPLv2](https://img.shields.io/badge/License-MPLv2-blue.svg)](https://opensource.org/licenses/MPL-2.0)
|
||||
|
||||
Instant messanger for Android that is fully decentralized. Messages are sent directly between
|
||||
devices via Bluetooth, without any central server. A simple flood-based routing is used for
|
||||
message propagation.
|
||||
Instant messenger for Android that is fully decentralized, and uses strong end-to-end
|
||||
encryption. Messages are sent directly between devices via Bluetooth or Internet, without any
|
||||
central server. Relay nodes are used to ensure message delivery, even if the target node is
|
||||
offline.
|
||||
|
||||
For details on how Ensichat works, you can check out my [bachelor thesis](docs/bachelor-thesis.pdf), and
|
||||
read the [protocol definition](PROTOCOL.md).
|
||||
|
||||
<img src="graphics/screenshot_phone_1.png" alt="screenshot 1" width="200" />
|
||||
<img src="graphics/screenshot_phone_2.png" alt="screenshot 2" width="200" />
|
||||
|
@ -13,14 +26,31 @@ message propagation.
|
|||
|
||||
[![Get it on Google Play](https://developer.android.com/images/brand/en_generic_rgb_wo_60.png)](https://play.google.com/store/apps/details?id=com.nutomic.ensichat) [![Get it on F-Droid](https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png)](https://f-droid.org/repository/browse/?fdid=com.nutomic.ensichat)
|
||||
|
||||
To set up a server, please follow the [instructions on the wiki](https://github.com/Nutomic/ensichat/wiki/Running-your-own-server).
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
To create a debug apk, run `./gradlew assembleDevDebug`. This requires at least Android Lollipop on your development device. If you don't have Lollipop, you can alternatively use `./gradlew assembleRelDebug`. However, this results in considerably slower incremental builds
|
||||
To setup a development environment, just install [Android Studio](https://developer.android.com/sdk/)
|
||||
and import the project.
|
||||
|
||||
To create a release apk, run `./gradlew assembleRelRelease`.
|
||||
Alternatively, you can use the command line. To create a debug apk, run `./gradlew assembleDevDebug`.
|
||||
This requires at least Android Lollipop on your development device. If you don't have 5.0 or higher,
|
||||
you have to use `./gradlew assembleRelDebug`. However, this results in considerably slower
|
||||
incremental builds. To create a release apk, run `./gradlew assembleRelRelease`.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
You can run the unit tests with `./gradlew test`. After connecting an Android device, you can run
|
||||
the Android tests with `./gradlew connectedDevDebugAndroidTest` (or
|
||||
`./gradlew connectedRelDebugAndroidTest` if your Android version is lower than 5.0).
|
||||
|
||||
To run integration tests for the core module, use `./gradlew integration:run`. If this fails (or
|
||||
is very slow), try changing the value of Crypto#PublicKeySize to 512 (in the core module).
|
||||
|
||||
License
|
||||
-------
|
||||
The project is licensed under the [MPLv2](LICENSE).
|
||||
|
||||
All code is licensed under the [GPL](LICENSE), v3 or later.
|
||||
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/).
|
||||
|
|
0
app/.gitignore → android/.gitignore
vendored
|
@ -1,23 +1,17 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'jp.leafytree.android-scala'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "jp.leafytree.gradle:gradle-android-scala-plugin:1.4"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "com.android.support:appcompat-v7:23.0.0"
|
||||
compile 'com.android.support:design:24.2.1'
|
||||
compile 'com.android.support:multidex:1.0.1'
|
||||
androidTestCompile "com.android.support:multidex-instrumentation:1.0.1",
|
||||
{ exclude module: "multidex" }
|
||||
compile "org.scala-lang:scala-library:2.11.7"
|
||||
compile 'com.google.guava:guava:18.0'
|
||||
compile 'org.scala-lang:scala-library:2.11.7'
|
||||
compile 'com.mobsandgeeks:adapter-kit:0.5.3'
|
||||
compile 'com.google.zxing:android-integration:3.3.0'
|
||||
compile 'com.google.zxing:core:3.3.0'
|
||||
compile 'org.slf4j:slf4j-android:1.7.21'
|
||||
compile project(path: ':core')
|
||||
androidTestCompile 'com.android.support:multidex-instrumentation:1.0.1',
|
||||
{ exclude module: 'multidex' }
|
||||
}
|
||||
|
||||
// RtlHardcoded behaviour differs between target API versions. We only care about API 15.
|
||||
|
@ -31,21 +25,23 @@ preBuild.doFirst {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "22.0.1"
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion "24.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.nutomic.ensichat"
|
||||
targetSdkVersion 23
|
||||
versionCode 7
|
||||
versionName "0.1.6"
|
||||
targetSdkVersion 24
|
||||
versionCode 17
|
||||
versionName "0.5.2"
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
|
||||
}
|
||||
|
||||
buildTypes.debug {
|
||||
applicationIdSuffix ".debug"
|
||||
testCoverageEnabled true
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
testCoverageEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
// Increasing minSdkVersion reduces compilation time for MultiDex.
|
|
@ -6,15 +6,18 @@ import android.test.AndroidTestCase
|
|||
|
||||
class BluetoothInterfaceTest extends AndroidTestCase {
|
||||
|
||||
private lazy val adapter = new BluetoothInterface(getContext, new Handler(), Message => Unit,
|
||||
() => Unit, Message => false)
|
||||
private lazy val btInterface = new BluetoothInterface(getContext, new Handler(), null)
|
||||
|
||||
/**
|
||||
* Test for issue [[https://github.com/Nutomic/ensichat/issues/3 #3]].
|
||||
*/
|
||||
def testStartBluetoothOff(): Unit = {
|
||||
BluetoothAdapter.getDefaultAdapter.disable()
|
||||
adapter.create()
|
||||
val btAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
if (btAdapter == null)
|
||||
return
|
||||
|
||||
btAdapter.disable()
|
||||
btInterface.create()
|
||||
}
|
||||
|
||||
}
|
|
@ -6,11 +6,15 @@
|
|||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
|
@ -30,11 +34,13 @@
|
|||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop" />
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.NoActionBar"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.AddContactsActivity"
|
||||
android:label="@string/add_contacts" >
|
||||
android:name=".activities.ConnectionsActivity"
|
||||
android:label="@string/connections"
|
||||
android:parentActivityName=".activities.MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.MainActivity" />
|
||||
|
@ -42,13 +48,20 @@
|
|||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/settings" >
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".activities.MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<service android:name=".protocol.ChatService" />
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".service.ChatService" />
|
||||
|
||||
</application>
|
||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 465 B |
BIN
android/src/main/res/drawable-hdpi/ic_add_white_24dp.png
Normal file
After Width: | Height: | Size: 127 B |
BIN
android/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
android/src/main/res/drawable-hdpi/ic_person_add_white_24dp.png
Normal file
After Width: | Height: | Size: 289 B |
BIN
android/src/main/res/drawable-hdpi/ic_qrcode_white_24dp.png
Normal file
After Width: | Height: | Size: 391 B |
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
BIN
android/src/main/res/drawable-mdpi/ic_add_white_24dp.png
Normal file
After Width: | Height: | Size: 88 B |
BIN
android/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
android/src/main/res/drawable-mdpi/ic_person_add_white_24dp.png
Normal file
After Width: | Height: | Size: 204 B |
BIN
android/src/main/res/drawable-mdpi/ic_qrcode_white_24dp.png
Normal file
After Width: | Height: | Size: 233 B |
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
BIN
android/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
Normal file
After Width: | Height: | Size: 97 B |
BIN
android/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
android/src/main/res/drawable-xhdpi/ic_person_add_white_24dp.png
Normal file
After Width: | Height: | Size: 329 B |
BIN
android/src/main/res/drawable-xhdpi/ic_qrcode_white_24dp.png
Normal file
After Width: | Height: | Size: 271 B |
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 759 B |
BIN
android/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png
Normal file
After Width: | Height: | Size: 97 B |
BIN
android/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 464 B |
BIN
android/src/main/res/drawable-xxhdpi/ic_qrcode_white_24dp.png
Normal file
After Width: | Height: | Size: 329 B |
BIN
android/src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png
Normal file
After Width: | Height: | Size: 102 B |
BIN
android/src/main/res/drawable-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 610 B |
BIN
android/src/main/res/drawable-xxxhdpi/ic_qrcode_white_24dp.png
Normal file
After Width: | Height: | Size: 380 B |
|
@ -26,7 +26,7 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/devices_empty"
|
||||
android:text="@string/no_connections"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/primary"
|
||||
android:layout_width="fill_parent"
|
|
@ -1,12 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="10"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/chat_background">
|
||||
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -41,6 +57,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/message_hint"
|
||||
android:textColorHint="#cccccc"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"/>
|
||||
|
87
android/src/main/res/layout/fragment_contacts.xml
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
android:contentInsetLeft="0dp"
|
||||
android:contentInsetStart="0dp"
|
||||
app:contentInsetLeft="0dp"
|
||||
app:contentInsetStart="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/title_holder"
|
||||
android:orientation="vertical"
|
||||
android:clickable="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingStart="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/title"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/subtitle"
|
||||
android:textSize="16sp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v7.widget.Toolbar>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_contacts_found"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_add_white_24dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
34
android/src/main/res/layout/fragment_identicon.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="25dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/identicon"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="25dp"
|
||||
android:layout_marginBottom="25dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
17
android/src/main/res/menu/connections.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/add_contact"
|
||||
android:title="@string/add_contact"
|
||||
android:icon="@drawable/ic_person_add_white_24dp"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/scan_qr"
|
||||
android:title="@string/scan_qr_code"
|
||||
android:icon="@drawable/ic_qrcode_white_24dp"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
|
@ -1,13 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/add_contact"
|
||||
android:title="@string/add_contacts"
|
||||
android:icon="@drawable/ic_action_add_person"
|
||||
app:showAsAction="ifRoom" />
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/share_app"
|
||||
|
@ -21,8 +14,4 @@
|
|||
android:id="@+id/settings"
|
||||
android:title="@string/settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/exit"
|
||||
android:title="@string/exit" />
|
||||
|
||||
</menu>
|
|
@ -14,4 +14,8 @@
|
|||
<!-- Background color for ChatFragment -->
|
||||
<color name="chat_background">#f3f2fb</color>
|
||||
|
||||
<color name="title_connections_error">#F44336</color>
|
||||
<color name="title_connections_warning">#FFEB3B</color>
|
||||
<color name="title_connections_ok">#8BC34A</color>
|
||||
|
||||
</resources>
|
|
@ -18,32 +18,42 @@
|
|||
<!-- Label for button to finish activity -->
|
||||
<string name="done">Done</string>
|
||||
|
||||
<!-- Toast shown if location permission is not granted -->
|
||||
<string name="toast_location_required">Location permission is required to scan for other Bluetooth devices</string>
|
||||
|
||||
<!-- MainActivity -->
|
||||
|
||||
<!-- Toast shown if user denies request to enable bluetooth -->
|
||||
<string name="bluetooth_required">Bluetooth is required for this app.</string>
|
||||
<string name="toast_bluetooth_denied">Please enable Bluetooth to connect with devices near you</string>
|
||||
|
||||
|
||||
<!-- ContactsFragment -->
|
||||
|
||||
<!-- Number of connections shown in actionbar -->
|
||||
<plurals name="title_connections">
|
||||
<item quantity="one">1 Connection</item>
|
||||
<item quantity="other">%s Connections</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Empty text for contacts list -->
|
||||
<string name="no_contacts_found">You haven\'t added any contacts yet</string>
|
||||
|
||||
<!-- Menu item to share this app's apk -->
|
||||
<string name="share_app">Share App</string>
|
||||
|
||||
<!-- Menu item to close app and stop service -->
|
||||
<string name="exit">Exit</string>
|
||||
<!-- ChatFragment -->
|
||||
|
||||
<!-- Hint text for the message EditText -->
|
||||
<string name="message_hint">Type a message</string>
|
||||
|
||||
|
||||
<!-- AddContactsActivity -->
|
||||
<!-- ConnectionsActivity -->
|
||||
|
||||
<!-- Activity title -->
|
||||
<string name="add_contacts">Add Contacts</string>
|
||||
<string name="connections">Connections</string>
|
||||
|
||||
<!-- Empty text for devices list -->
|
||||
<string name="devices_empty">Searching for Users\nRange: ~10m</string>
|
||||
<string name="no_connections">Searching for Users\nRange: ~10m</string>
|
||||
|
||||
<!-- Alertdialog message to add new contact -->
|
||||
<string name="dialog_add_contact">Do you want to add %1$s as contact?</string>
|
||||
|
@ -51,6 +61,17 @@
|
|||
<!-- Toast shown after contact has been added -->
|
||||
<string name="toast_contact_added">Contact added</string>
|
||||
|
||||
<!-- Toast shown when clicking a user that is already a contact -->
|
||||
<string name="contact_already_added">You have already added %1$s as a contact</string>
|
||||
|
||||
<string name="enter_id">Enter user ID</string>
|
||||
|
||||
<string name="invalid_address">Invalid address</string>
|
||||
|
||||
<string name="add_contact">Add Contact</string>
|
||||
|
||||
<string name="scan_qr_code">Scan QR-Code</string>
|
||||
|
||||
|
||||
<!-- SettingsActivity -->
|
||||
|
||||
|
@ -72,8 +93,8 @@
|
|||
<!-- Preference title -->
|
||||
<string name="notification_sounds">Notification Sounds</string>
|
||||
|
||||
<!-- Preference title (debug only)-->
|
||||
<string name="max_connections" translatable="false">Maximum Number of Connections</string>
|
||||
<!-- Preference title -->
|
||||
<string name="servers">Servers</string>
|
||||
|
||||
<!-- Preference title -->
|
||||
<string name="report_issue">Report Issue</string>
|
||||
|
@ -86,15 +107,26 @@
|
|||
<!-- Preference title -->
|
||||
<string name="version">Version</string>
|
||||
|
||||
<!-- IdenticonFragment -->
|
||||
|
||||
<!-- UserInfoFragment -->
|
||||
|
||||
<!-- Device address label and value -->
|
||||
<string name="address_colon">Address: %1$s</string>
|
||||
|
||||
<string name="ensichat_user_address">Ensichat User Address</string>
|
||||
|
||||
<string name="address_copied_to_clipboard">Address copied to clipboard</string>
|
||||
|
||||
|
||||
<!-- ChatService -->
|
||||
|
||||
<!-- Notification text for incoming message -->
|
||||
<string name="notification_message">New message!</string>
|
||||
|
||||
<!-- Info text for persistent notification -->
|
||||
<plurals name="notification_connections">
|
||||
<item quantity="one">Connected to %1$s device</item>
|
||||
<item quantity="other">Connected to %1$s devices</item>
|
||||
</plurals>
|
||||
|
||||
</resources>
|
|
@ -9,6 +9,11 @@
|
|||
<item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="AppTheme">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AlertDialogTheme" parent="@style/Theme.AppCompat.Light.Dialog">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
|
@ -4,30 +4,27 @@
|
|||
|
||||
<EditTextPreference
|
||||
android:title="@string/user_name"
|
||||
android:key="user_name" />
|
||||
android:key="user_name"
|
||||
android:inputType="textCapWords"/>
|
||||
|
||||
<EditTextPreference
|
||||
android:title="@string/user_status"
|
||||
android:key="user_status" />
|
||||
android:key="user_status"
|
||||
android:inputType="textCapSentences" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:title="@string/notification_sounds"
|
||||
android:key="notification_sounds"
|
||||
android:defaultValue="@bool/default_notification_sounds" />
|
||||
android:key="notification_sounds" />
|
||||
|
||||
<EditTextPreference
|
||||
android:title="@string/scan_interval_seconds"
|
||||
android:key="scan_interval_seconds"
|
||||
android:defaultValue="@string/default_scan_interval"
|
||||
android:inputType="number"
|
||||
android:numeric="integer" />
|
||||
|
||||
<EditTextPreference
|
||||
android:title="@string/max_connections"
|
||||
android:key="max_connections"
|
||||
android:defaultValue="@string/default_max_connections"
|
||||
android:inputType="number"
|
||||
android:numeric="integer" />
|
||||
android:title="@string/servers"
|
||||
android:key="servers" />
|
||||
|
||||
<Preference
|
||||
android:title="@string/report_issue"
|
13
android/src/main/scala/com/nutomic/ensichat/App.scala
Normal file
|
@ -0,0 +1,13 @@
|
|||
package com.nutomic.ensichat
|
||||
|
||||
import android.support.multidex.MultiDexApplication
|
||||
import com.nutomic.ensichat.util.PRNGFixes
|
||||
|
||||
class App extends MultiDexApplication {
|
||||
|
||||
override def onCreate(): Unit = {
|
||||
super.onCreate()
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog.Builder
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.content._
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.view._
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget._
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.service.CallbackHandler
|
||||
import com.nutomic.ensichat.views.UsersAdapter
|
||||
|
||||
/**
|
||||
* Lists all nearby, connected devices and allows adding them to be added as contacts.
|
||||
*/
|
||||
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
||||
|
||||
private lazy val adapter = new UsersAdapter(this)
|
||||
|
||||
/**
|
||||
* Initializes layout, registers connection and message listeners.
|
||||
*/
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
getSupportActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
setContentView(R.layout.activity_connections)
|
||||
val list = findViewById(android.R.id.list).asInstanceOf[ListView]
|
||||
list.setAdapter(adapter)
|
||||
list.setOnItemClickListener(this)
|
||||
list.setEmptyView(findViewById(android.R.id.empty))
|
||||
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(CallbackHandler.ActionConnectionsChanged)
|
||||
filter.addAction(CallbackHandler.ActionContactsUpdated)
|
||||
LocalBroadcastManager.getInstance(this)
|
||||
.registerReceiver(onContactsUpdatedReceiver, filter)
|
||||
}
|
||||
|
||||
override def onResume(): Unit = {
|
||||
super.onResume()
|
||||
runOnServiceConnected(() => {
|
||||
updateConnections()
|
||||
})
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
super.onDestroy()
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(onContactsUpdatedReceiver)
|
||||
}
|
||||
|
||||
override def onCreateOptionsMenu(menu: Menu): Boolean = {
|
||||
getMenuInflater.inflate(R.menu.connections, menu)
|
||||
true
|
||||
}
|
||||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||
case R.id.add_contact =>
|
||||
val et = new EditText(this)
|
||||
new Builder(this)
|
||||
.setTitle(R.string.enter_id)
|
||||
.setView(et)
|
||||
.setPositiveButton(android.R.string.ok, new OnClickListener {
|
||||
override def onClick(dialog: DialogInterface, which: Int): Unit = {
|
||||
addContact(et.getText.toString)
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
true
|
||||
case R.id.scan_qr =>
|
||||
new IntentIntegrator(this).initiateScan
|
||||
true
|
||||
case android.R.id.home =>
|
||||
NavUtils.navigateUpFromSameTask(this)
|
||||
true
|
||||
case _ =>
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates adding the device as contact if it hasn't been added yet.
|
||||
*/
|
||||
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit =
|
||||
addContact(adapter.getItem(position).address.toString)
|
||||
|
||||
/**
|
||||
* Receives value of scanned QR code and sets it as device ID.
|
||||
*/
|
||||
override def onActivityResult(requestCode: Int, resultCode: Int, intent: Intent) {
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
|
||||
if (scanResult != null && resultCode == Activity.RESULT_OK) {
|
||||
addContact(scanResult.getContents)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the address, and shows a dialog to add the user as a contact.
|
||||
*
|
||||
* Displays a warning toast if the address is invalid or if the user is already a contact.
|
||||
*/
|
||||
private def addContact(address: String): Unit = {
|
||||
val parsedAddress =
|
||||
try {
|
||||
new Address(address)
|
||||
} catch {
|
||||
case e: IllegalArgumentException =>
|
||||
Toast.makeText(this, R.string.invalid_address, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val user = service.get.getUser(parsedAddress)
|
||||
|
||||
if (database.get.getContacts.map(_.address).contains(user.address)) {
|
||||
val text = getString(R.string.contact_already_added, user.name)
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
new Builder(this)
|
||||
.setMessage(getString(R.string.dialog_add_contact, user.name))
|
||||
.setPositiveButton(android.R.string.yes, new OnClickListener {
|
||||
override def onClick(dialog: DialogInterface, which: Int): Unit = {
|
||||
service.get.addContact(user)
|
||||
Toast.makeText(ConnectionsActivity.this, R.string.toast_contact_added, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches connections and displays them (excluding contacts).
|
||||
*/
|
||||
private val onContactsUpdatedReceiver = new BroadcastReceiver() {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
runOnUiThread(new Runnable {
|
||||
override def run(): Unit = updateConnections()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private def updateConnections(): Unit = {
|
||||
adapter.clear()
|
||||
service.get.connections().map(a => service.get.getUser(a))
|
||||
.foreach(adapter.add)
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,7 @@ package com.nutomic.ensichat.activities
|
|||
import android.content.{ComponentName, Context, Intent, ServiceConnection}
|
||||
import android.os.{Bundle, IBinder}
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import com.nutomic.ensichat.protocol.{ChatService, ChatServiceBinder}
|
||||
import com.nutomic.ensichat.service.ChatService
|
||||
|
||||
/**
|
||||
* Connects to [[ChatService]] and provides access to it.
|
||||
|
@ -37,7 +37,7 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
|
|||
* Clears the list containing them.
|
||||
*/
|
||||
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
|
||||
val binder = iBinder.asInstanceOf[ChatServiceBinder]
|
||||
val binder = iBinder.asInstanceOf[ChatService.Binder]
|
||||
chatService = Option(binder.service)
|
||||
listeners.foreach(_())
|
||||
listeners = Set.empty
|
||||
|
@ -58,8 +58,10 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
|
|||
/**
|
||||
* Returns the [[ChatService]].
|
||||
*
|
||||
* Should only be called after [[runOnServiceConnected]] callback was called.
|
||||
* Will only be set after [[runOnServiceConnected]].
|
||||
*/
|
||||
def service = chatService.get
|
||||
def service = chatService.map(_.getConnectionHandler)
|
||||
|
||||
def database = chatService.map(_.database)
|
||||
|
||||
}
|
|
@ -1,17 +1,22 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.{Context, Intent}
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.inputmethod.{EditorInfo, InputMethodManager}
|
||||
import android.view.{KeyEvent, View}
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget.{Button, EditText, TextView}
|
||||
import android.widget.{Button, EditText, TextView, Toast}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.fragments.SettingsFragment
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
||||
|
||||
/**
|
||||
* Shown on first start, lets the user enter their name.
|
||||
|
@ -19,6 +24,7 @@ import com.nutomic.ensichat.fragments.SettingsFragment
|
|||
class FirstStartActivity extends AppCompatActivity with OnEditorActionListener with OnClickListener {
|
||||
|
||||
private val KeyIsFirstStart = "first_start"
|
||||
private val RequestLocationPermission = 127
|
||||
|
||||
private lazy val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
private lazy val imm = getSystemService(Context.INPUT_METHOD_SERVICE)
|
||||
|
@ -30,19 +36,24 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
|
|||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!preferences.getBoolean(KeyIsFirstStart, true)) {
|
||||
startMainActivity()
|
||||
return
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_first_start)
|
||||
setTitle(R.string.welcome)
|
||||
|
||||
username.setText(BluetoothAdapter.getDefaultAdapter.getName.trim)
|
||||
val name = Option(BluetoothAdapter.getDefaultAdapter).map(_.getName.trim).getOrElse("")
|
||||
username.setText(name)
|
||||
username.setOnEditorActionListener(this)
|
||||
done.setOnClickListener(this)
|
||||
|
||||
imm.showSoftInput(username, InputMethodManager.SHOW_IMPLICIT)
|
||||
val permission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
if (preferences.getBoolean(KeyIsFirstStart, true)) {
|
||||
imm.showSoftInput(username, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
else if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
requestLocationPermission()
|
||||
}
|
||||
else {
|
||||
startMainActivity()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,7 +71,7 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
|
|||
override def onClick(v: View): Unit = save()
|
||||
|
||||
/**
|
||||
* Saves values and calls [[startMainActivity]].
|
||||
* Saves username and default settings values, then calls [[startMainActivity]].
|
||||
*/
|
||||
private def save(): Unit = {
|
||||
imm.hideSoftInputFromWindow(username.getWindowToken, 0)
|
||||
|
@ -68,11 +79,27 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
|
|||
preferences
|
||||
.edit()
|
||||
.putBoolean(KeyIsFirstStart, false)
|
||||
.putString(SettingsFragment.KeyUserName, username.getText.toString.trim)
|
||||
.putString(SettingsFragment.KeyUserStatus, getString(R.string.default_user_status))
|
||||
.putString(KeyUserName, username.getText.toString.trim)
|
||||
.putString(KeyUserStatus, SettingsInterface.DefaultUserStatus)
|
||||
.putBoolean(KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
|
||||
.putString(KeyScanInterval, DefaultScanInterval.toString)
|
||||
.putString(KeyAddresses, DefaultAddresses)
|
||||
.apply()
|
||||
|
||||
startMainActivity()
|
||||
requestLocationPermission()
|
||||
}
|
||||
|
||||
private def requestLocationPermission(): Unit =
|
||||
ActivityCompat.requestPermissions(this, Array(Manifest.permission.ACCESS_COARSE_LOCATION), RequestLocationPermission)
|
||||
|
||||
override def onRequestPermissionsResult(requestCode: Int,
|
||||
permissions: Array[String], grantResults: Array[Int]): Unit = requestCode match {
|
||||
case RequestLocationPermission =>
|
||||
if (grantResults.length > 0 && grantResults(0) == PackageManager.PERMISSION_GRANTED) {
|
||||
startMainActivity()
|
||||
} else {
|
||||
Toast.makeText(this, R.string.toast_location_required, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
def startMainActivity(): Unit = {
|
|
@ -4,11 +4,12 @@ import android.app.Activity
|
|||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content._
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.fragments.{ChatFragment, ContactsFragment}
|
||||
import com.nutomic.ensichat.protocol.Address
|
||||
|
||||
object MainActivity {
|
||||
|
||||
|
@ -24,6 +25,8 @@ object MainActivity {
|
|||
|
||||
val ExtraAddress = "address"
|
||||
|
||||
val PrefWasBluetoothEnabled = "was_bluetooth_enabled"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,7 +47,14 @@ class MainActivity extends EnsichatActivity {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
if (getIntent.getAction == MainActivity.ActionRequestBluetooth) {
|
||||
if (getIntent.getAction == MainActivity.ActionRequestBluetooth &&
|
||||
Option(BluetoothAdapter.getDefaultAdapter).isDefined) {
|
||||
val btAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putBoolean(MainActivity.PrefWasBluetoothEnabled, btAdapter.isEnabled)
|
||||
.apply()
|
||||
|
||||
val intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
|
||||
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
|
||||
startActivityForResult(intent, RequestSetDiscoverable)
|
||||
|
@ -89,8 +99,7 @@ class MainActivity extends EnsichatActivity {
|
|||
requestCode match {
|
||||
case RequestSetDiscoverable =>
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(this, R.string.bluetooth_required, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
Toast.makeText(this, R.string.toast_bluetooth_denied, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +113,7 @@ class MainActivity extends EnsichatActivity {
|
|||
.detach(contactsFragment)
|
||||
.add(android.R.id.content, new ChatFragment(address))
|
||||
.commit()
|
||||
getSupportActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
Option(getSupportActionBar).foreach(_.setDisplayHomeAsUpEnabled(true))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,7 +135,8 @@ class MainActivity extends EnsichatActivity {
|
|||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||
case android.R.id.home =>
|
||||
onBackPressed()
|
||||
if (currentChat.isDefined)
|
||||
onBackPressed()
|
||||
true;
|
||||
case _ =>
|
||||
super.onOptionsItemSelected(item);
|
|
@ -8,22 +8,30 @@ import android.util.Log
|
|||
/**
|
||||
* Attempts to connect to another device and calls [[onConnected]] on success.
|
||||
*/
|
||||
class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
||||
class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
||||
|
||||
private val Tag = "ConnectThread"
|
||||
|
||||
private val socket =
|
||||
device.btDevice.get.createInsecureRfcommSocketToServiceRecord(BluetoothInterface.AppUuid)
|
||||
private val socket = try {
|
||||
Option(device.btDevice.get.createInsecureRfcommSocketToServiceRecord(BluetoothInterface.AppUuid))
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
Log.w(Tag, "Failed to open Bluetooth connection", e)
|
||||
None
|
||||
}
|
||||
|
||||
override def run(): Unit = {
|
||||
if (socket.isEmpty)
|
||||
return
|
||||
|
||||
Log.i(Tag, "Connecting to " + device.toString)
|
||||
try {
|
||||
socket.connect()
|
||||
socket.get.connect()
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
Log.v(Tag, "Failed to connect to " + device.toString, e)
|
||||
try {
|
||||
socket.close()
|
||||
socket.get.close()
|
||||
} catch {
|
||||
case e2: IOException =>
|
||||
Log.e(Tag, "Failed to close socket", e2)
|
||||
|
@ -32,7 +40,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
|
|||
}
|
||||
|
||||
Log.i(Tag, "Successfully connected to device " + device.name)
|
||||
onConnected(new Device(device.btDevice.get, true), socket)
|
||||
onConnected(new Device(device.btDevice.get, true), socket.get)
|
||||
}
|
||||
|
||||
}
|
|
@ -7,12 +7,14 @@ import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
|||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import com.google.common.collect.HashBiMap
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.fragments.SettingsFragment
|
||||
import com.nutomic.ensichat.protocol.ChatService.InterfaceHandler
|
||||
import com.nutomic.ensichat.protocol._
|
||||
import com.nutomic.ensichat.protocol.body.ConnectionInfo
|
||||
import com.nutomic.ensichat.core.interfaces.{SettingsInterface, TransmissionInterface}
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import com.nutomic.ensichat.core.messages.body.ConnectionInfo
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.core.ConnectionHandler
|
||||
import com.nutomic.ensichat.service.ChatService
|
||||
import org.joda.time.{DateTime, Duration}
|
||||
|
||||
import scala.collection.immutable.HashMap
|
||||
|
||||
|
@ -29,27 +31,25 @@ object BluetoothInterface {
|
|||
* Handles all Bluetooth connectivity.
|
||||
*/
|
||||
class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||
onMessageReceived: Message => Unit, callConnectionListeners: () => Unit,
|
||||
onConnectionOpened: (Message) => Boolean)
|
||||
extends InterfaceHandler {
|
||||
connectionHandler: ConnectionHandler) extends TransmissionInterface {
|
||||
|
||||
private val Tag = "BluetoothInterface"
|
||||
|
||||
private lazy val btAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
|
||||
private lazy val crypto = new Crypto(context)
|
||||
private lazy val crypto = ChatService.newCrypto(context)
|
||||
|
||||
private var devices = new HashMap[Device.ID, Device]()
|
||||
|
||||
private var connections = new HashMap[Device.ID, TransferThread]()
|
||||
private var connections = new HashMap[Device.ID, BluetoothTransferThread]()
|
||||
|
||||
private var listenThread: Option[ListenThread] = None
|
||||
private var listenThread: Option[BluetoothListenThread] = None
|
||||
|
||||
private var cancelDiscovery = false
|
||||
|
||||
private var discovered = Set[Device]()
|
||||
|
||||
private val addressDeviceMap = HashBiMap.create[Address, Device.ID]()
|
||||
private var addressDeviceMap = new HashMap[Address, Device.ID]()
|
||||
|
||||
/**
|
||||
* Initializes and starts discovery and listening.
|
||||
|
@ -71,7 +71,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
* Stops discovery and listening.
|
||||
*/
|
||||
override def destroy(): Unit = {
|
||||
listenThread.get.cancel()
|
||||
listenThread.foreach(_.cancel())
|
||||
listenThread = None
|
||||
cancelDiscovery = true
|
||||
try {
|
||||
|
@ -89,7 +89,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
* Starts discovery and listening.
|
||||
*/
|
||||
private def startBluetoothConnections(): Unit = {
|
||||
listenThread = Some(new ListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
|
||||
listenThread = Some(new BluetoothListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
|
||||
listenThread.get.start()
|
||||
cancelDiscovery = false
|
||||
discover()
|
||||
|
@ -108,8 +108,8 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
}
|
||||
|
||||
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val scanInterval = pm.getString(SettingsFragment.KeyScanInterval,
|
||||
context.getResources.getString(R.string.default_scan_interval)).toInt * 1000
|
||||
val scanInterval =
|
||||
pm.getString(SettingsInterface.KeyScanInterval, SettingsInterface.DefaultScanInterval.toString).toInt * 1000
|
||||
mainHandler.postDelayed(new Runnable {
|
||||
override def run(): Unit = discover()
|
||||
}, scanInterval)
|
||||
|
@ -131,7 +131,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
discovered.filterNot(d => connections.keySet.contains(d.id))
|
||||
.foreach { d =>
|
||||
new ConnectThread(d, connectionOpened).start()
|
||||
new BluetoothConnectThread(d, connectionOpened).start()
|
||||
devices += (d.id -> d)
|
||||
}
|
||||
discovered = Set[Device]()
|
||||
|
@ -165,18 +165,20 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
def connectionOpened(device: Device, socket: BluetoothSocket): Unit = {
|
||||
devices += (device.id -> device)
|
||||
connections += (device.id ->
|
||||
new TransferThread(context, device, socket, this, crypto, onReceiveMessage))
|
||||
new BluetoothTransferThread(context, device, socket, this, crypto, onReceiveMessage))
|
||||
connections(device.id).start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes device from active connections.
|
||||
*/
|
||||
def onConnectionClosed(device: Device, socket: BluetoothSocket): Unit = {
|
||||
devices -= device.id
|
||||
connections -= device.id
|
||||
callConnectionListeners()
|
||||
addressDeviceMap.inverse().remove(device.id)
|
||||
def onConnectionClosed(connectionOpened: DateTime, deviceId: Device.ID): Unit = {
|
||||
val address = getAddressForDevice(deviceId)
|
||||
devices -= deviceId
|
||||
connections -= deviceId
|
||||
addressDeviceMap = addressDeviceMap.filterNot(_._2 == deviceId)
|
||||
val connectionDuration = new Duration(connectionOpened, DateTime.now)
|
||||
connectionHandler.onConnectionClosed(address, connectionDuration)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,23 +193,31 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
case info: ConnectionInfo =>
|
||||
val address = crypto.calculateAddress(info.key)
|
||||
// Service.onConnectionOpened sends message, so mapping already needs to be in place.
|
||||
addressDeviceMap.put(address, device)
|
||||
if (!onConnectionOpened(msg))
|
||||
addressDeviceMap.remove(address)
|
||||
addressDeviceMap += (address -> device)
|
||||
if (!connectionHandler.onConnectionOpened(msg))
|
||||
addressDeviceMap -= address
|
||||
case _ =>
|
||||
onMessageReceived(msg)
|
||||
connectionHandler.onMessageReceived(msg, getAddressForDevice(device))
|
||||
}
|
||||
|
||||
private def getAddressForDevice(device: Device.ID) =
|
||||
addressDeviceMap.find(_._2 == device).get._1
|
||||
|
||||
/**
|
||||
* Sends the message to nextHop.
|
||||
*/
|
||||
override def send(nextHop: Address, msg: Message): Unit =
|
||||
connections.get(addressDeviceMap.get(nextHop)).foreach(_.send(msg))
|
||||
override def send(nextHop: Address, msg: Message): Unit = {
|
||||
addressDeviceMap
|
||||
.find(_._1 == nextHop || Address.Broadcast == nextHop)
|
||||
.map(i => connections.get(i._2))
|
||||
.getOrElse(None)
|
||||
.foreach(_.send(msg))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all active Bluetooth connections.
|
||||
*/
|
||||
def getConnections: Set[Address] =
|
||||
connections.map(x => addressDeviceMap.inverse().get(x._1)).toSet
|
||||
override def getConnections: Set[Address] =
|
||||
connections.map( c => getAddressForDevice(c._1)).toSet
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ import android.util.Log
|
|||
*
|
||||
* @param name Service name to broadcast.
|
||||
*/
|
||||
class ListenThread(name: String, adapter: BluetoothAdapter,
|
||||
class BluetoothListenThread(name: String, adapter: BluetoothAdapter,
|
||||
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
||||
|
||||
private val Tag = "ListenThread"
|
|
@ -3,12 +3,16 @@ package com.nutomic.ensichat.bluetooth
|
|||
import java.io._
|
||||
|
||||
import android.bluetooth.{BluetoothDevice, BluetoothSocket}
|
||||
import android.content.{IntentFilter, Intent, Context, BroadcastReceiver}
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.protocol._
|
||||
import com.nutomic.ensichat.protocol.body.ConnectionInfo
|
||||
import com.nutomic.ensichat.protocol.header.MessageHeader
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import Message.ReadMessageException
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import com.nutomic.ensichat.core.messages.body.ConnectionInfo
|
||||
import com.nutomic.ensichat.core.messages.header.MessageHeader
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.core.util.Crypto
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Transfers data between connnected devices.
|
||||
|
@ -17,8 +21,11 @@ import Message.ReadMessageException
|
|||
* @param socket An open socket to the given device.
|
||||
* @param onReceive Called when a message was received from the other device.
|
||||
*/
|
||||
class TransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface,
|
||||
crypto: Crypto, onReceive: (Message, Device.ID) => Unit) extends Thread {
|
||||
class BluetoothTransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
||||
handler: BluetoothInterface, crypto: Crypto,
|
||||
onReceive: (Message, Device.ID) => Unit) extends Thread {
|
||||
|
||||
private val connectionOpened = DateTime.now
|
||||
|
||||
private val Tag = "TransferThread"
|
||||
|
||||
|
@ -30,6 +37,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
|||
} catch {
|
||||
case e: IOException =>
|
||||
Log.e(Tag, "Failed to open stream", e)
|
||||
close()
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -39,6 +47,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
|||
} catch {
|
||||
case e: IOException =>
|
||||
Log.e(Tag, "Failed to open stream", e)
|
||||
close()
|
||||
null
|
||||
}
|
||||
|
||||
|
@ -59,16 +68,14 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
|||
new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED))
|
||||
|
||||
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
|
||||
Address.Null, Address.Null, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||
Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
|
||||
|
||||
while (socket.isConnected) {
|
||||
try {
|
||||
if (inStream.available() > 0) {
|
||||
val msg = Message.read(inStream)
|
||||
val msg = Message.read(inStream)
|
||||
Log.v(Tag, "Received " + msg)
|
||||
|
||||
onReceive(msg, device.id)
|
||||
Log.v(Tag, "Receiving " + msg)
|
||||
}
|
||||
onReceive(msg, device.id)
|
||||
} catch {
|
||||
case e @ (_: ReadMessageException | _: IOException) =>
|
||||
Log.w(Tag, "Failed to read incoming message", e)
|
||||
|
@ -102,7 +109,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
|||
} catch {
|
||||
case e: IOException => Log.e(Tag, "Failed to close socket", e);
|
||||
} finally {
|
||||
handler.onConnectionClosed(new Device(device.btDevice.get, false), null)
|
||||
handler.onConnectionClosed(connectionOpened, device.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.app.ListFragment
|
|||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.Bundle
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.{KeyEvent, LayoutInflater, View, ViewGroup}
|
||||
|
@ -11,9 +12,11 @@ import android.widget.TextView.OnEditorActionListener
|
|||
import android.widget._
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.EnsichatActivity
|
||||
import com.nutomic.ensichat.protocol.body.Text
|
||||
import com.nutomic.ensichat.protocol.{Address, ChatService, Message}
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.core.messages.body.Text
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.core.ConnectionHandler
|
||||
import com.nutomic.ensichat.service.CallbackHandler
|
||||
import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter}
|
||||
|
||||
/**
|
||||
|
@ -29,11 +32,11 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
this.address = address
|
||||
}
|
||||
|
||||
private lazy val database = new Database(getActivity)
|
||||
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
|
||||
private var address: Address = _
|
||||
|
||||
private var chatService: ChatService = _
|
||||
private var chatService: ConnectionHandler = _
|
||||
|
||||
private var sendButton: Button = _
|
||||
|
||||
|
@ -46,14 +49,13 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
override def onActivityCreated(savedInstanceState: Bundle): Unit = {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
activity.runOnServiceConnected(() => {
|
||||
chatService = activity.service
|
||||
chatService = activity.service.get
|
||||
|
||||
database.getContact(address).foreach(c => getActivity.setTitle(c.name))
|
||||
activity.database.get.getContact(address).foreach(c => getActivity.setTitle(c.name))
|
||||
|
||||
adapter = new DatesAdapter(getActivity,
|
||||
new MessagesAdapter(getActivity, database.getMessagesCursor(address, None), address))
|
||||
new MessagesAdapter(getActivity, activity.database.get.getMessages(address), address))
|
||||
|
||||
if (listView != null) {
|
||||
listView.setAdapter(adapter)
|
||||
|
@ -62,8 +64,11 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
}
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
savedInstanceState: Bundle): View = {
|
||||
val view: View = inflater.inflate(R.layout.fragment_chat, container, false)
|
||||
savedInstanceState: Bundle): View = {
|
||||
val view = inflater.inflate(R.layout.fragment_chat, container, false)
|
||||
val toolbar = view.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
activity.setSupportActionBar(toolbar)
|
||||
activity.getSupportActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
sendButton = view.findViewById(R.id.send).asInstanceOf[Button]
|
||||
sendButton.setOnClickListener(this)
|
||||
messageText = view.findViewById(R.id.message).asInstanceOf[EditText]
|
||||
|
@ -88,7 +93,7 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
address = new Address(savedInstanceState.getByteArray("address"))
|
||||
|
||||
LocalBroadcastManager.getInstance(getActivity)
|
||||
.registerReceiver(onMessageReceivedReceiver, new IntentFilter(ChatService.ActionMessageReceived))
|
||||
.registerReceiver(onMessageReceivedReceiver, new IntentFilter(CallbackHandler.ActionMessageReceived))
|
||||
}
|
||||
|
||||
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||
|
@ -119,13 +124,14 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
*/
|
||||
private val onMessageReceivedReceiver = new BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
val msg = intent.getSerializableExtra(ChatService.ExtraMessage).asInstanceOf[Message]
|
||||
val msg = intent.getSerializableExtra(CallbackHandler.ExtraMessage).asInstanceOf[Message]
|
||||
if (!Set(msg.header.origin, msg.header.target).contains(address))
|
||||
return
|
||||
|
||||
msg.body match {
|
||||
case _: Text =>
|
||||
adapter.changeCursor(database.getMessagesCursor(address, None))
|
||||
val messages = activity.database.get.getMessages(address)
|
||||
adapter.replaceItems(messages)
|
||||
case _ =>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
import java.io.File
|
||||
|
||||
import android.app.ListFragment
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.content.{ContextCompat, LocalBroadcastManager}
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View.OnClickListener
|
||||
import android.view._
|
||||
import android.widget.{ListView, TextView}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||
import com.nutomic.ensichat.service.{CallbackHandler, ChatService}
|
||||
import com.nutomic.ensichat.views.UsersAdapter
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* Lists all nearby, connected devices.
|
||||
*/
|
||||
class ContactsFragment extends ListFragment with OnClickListener {
|
||||
|
||||
private lazy val adapter = new UsersAdapter(getActivity)
|
||||
|
||||
private lazy val database = activity.database.get
|
||||
|
||||
private lazy val lbm = LocalBroadcastManager.getInstance(getActivity)
|
||||
|
||||
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
|
||||
private var title: TextView = _
|
||||
private var subtitle: TextView = _
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setListAdapter(adapter)
|
||||
setHasOptionsMenu(true)
|
||||
lbm.registerReceiver(onContactsUpdatedListener, new IntentFilter(CallbackHandler.ActionContactsUpdated))
|
||||
lbm.registerReceiver(onConnectionsChangedListener, new IntentFilter(CallbackHandler.ActionConnectionsChanged))
|
||||
}
|
||||
|
||||
override def onResume(): Unit = {
|
||||
super.onResume()
|
||||
activity.runOnServiceConnected(() => {
|
||||
adapter.clear()
|
||||
adapter.addAll(database.getContacts)
|
||||
updateConnections()
|
||||
})
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
super.onDestroy()
|
||||
lbm.unregisterReceiver(onContactsUpdatedListener)
|
||||
lbm.unregisterReceiver(onConnectionsChangedListener)
|
||||
}
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
savedInstanceState: Bundle): View = {
|
||||
val v = inflater.inflate(R.layout.fragment_contacts, container, false)
|
||||
val toolbar = v.findViewById(R.id.toolbar).asInstanceOf[Toolbar]
|
||||
v.findViewById(R.id.title_holder).setOnClickListener(this)
|
||||
activity.setSupportActionBar(toolbar)
|
||||
toolbar.setNavigationIcon(R.drawable.ic_launcher)
|
||||
title = v.findViewById(R.id.title).asInstanceOf[TextView]
|
||||
subtitle = v.findViewById(R.id.subtitle).asInstanceOf[TextView]
|
||||
val fab = v.findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
|
||||
fab.setOnClickListener(this)
|
||||
updateConnections()
|
||||
v
|
||||
}
|
||||
|
||||
override def onCreateOptionsMenu(menu: Menu, inflater: MenuInflater): Unit = {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.main, menu)
|
||||
}
|
||||
|
||||
override def onClick(v: View): Unit =
|
||||
startActivity(new Intent(getActivity, classOf[ConnectionsActivity]))
|
||||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||
case R.id.share_app =>
|
||||
val pm = getActivity.getPackageManager
|
||||
val ai = pm.getInstalledApplications(0).find(_.sourceDir.contains(getActivity.getPackageName))
|
||||
val intent = new Intent()
|
||||
intent.setAction(Intent.ACTION_SEND)
|
||||
intent.setType("*/*")
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(ai.get.sourceDir)))
|
||||
startActivity(intent)
|
||||
true
|
||||
case R.id.my_address =>
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(getActivity)
|
||||
val fragment = new UserInfoFragment()
|
||||
val bundle = new Bundle()
|
||||
bundle.putString(
|
||||
UserInfoFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString)
|
||||
bundle.putString(
|
||||
UserInfoFragment.ExtraUserName, prefs.getString(SettingsInterface.KeyUserName, ""))
|
||||
fragment.setArguments(bundle)
|
||||
fragment.show(getFragmentManager, "dialog")
|
||||
true
|
||||
case R.id.settings =>
|
||||
startActivity(new Intent(getActivity, classOf[SettingsActivity]))
|
||||
true
|
||||
case _ =>
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a chat with the clicked device.
|
||||
*/
|
||||
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit =
|
||||
getActivity.asInstanceOf[MainActivity].openChat(adapter.getItem(position).address)
|
||||
|
||||
private val onContactsUpdatedListener = new BroadcastReceiver() {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
getActivity.runOnUiThread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
adapter.clear()
|
||||
database.getContacts.foreach(adapter.add)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private val onConnectionsChangedListener = new BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = updateConnections()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates TextViews in actionbar with current connections.
|
||||
*/
|
||||
private def updateConnections(): Unit = {
|
||||
if (activity.service.isEmpty || title == null)
|
||||
return
|
||||
|
||||
val service = activity.service.get
|
||||
val connections = service.connections()
|
||||
val count = connections.size
|
||||
val color = count match {
|
||||
case 0 => R.color.title_connections_error
|
||||
case 1 => R.color.title_connections_warning
|
||||
case _ => R.color.title_connections_ok
|
||||
}
|
||||
|
||||
title.setText(getResources.getQuantityString(R.plurals.title_connections, count, count.toString))
|
||||
title.setTextColor(ContextCompat.getColor(getActivity, color))
|
||||
|
||||
subtitle.setText(connections.map(service.getUser(_).name).mkString(", "))
|
||||
subtitle.setVisibility(if (count == 0) View.GONE else View.VISIBLE)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.content.{Intent, SharedPreferences}
|
||||
import android.os.Bundle
|
||||
import android.preference.{PreferenceFragment, PreferenceManager}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.EnsichatActivity
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
||||
import com.nutomic.ensichat.core.messages.body.UserInfo
|
||||
import com.nutomic.ensichat.fragments.SettingsFragment._
|
||||
import com.nutomic.ensichat.service.ChatService
|
||||
|
||||
object SettingsFragment {
|
||||
val Version = "version"
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings screen.
|
||||
*/
|
||||
class SettingsFragment extends PreferenceFragment with OnSharedPreferenceChangeListener {
|
||||
|
||||
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
|
||||
private lazy val version = findPreference(Version)
|
||||
|
||||
private lazy val prefs = PreferenceManager.getDefaultSharedPreferences(getActivity)
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
addPreferencesFromResource(R.xml.settings)
|
||||
|
||||
val packageInfo = getActivity.getPackageManager.getPackageInfo(getActivity.getPackageName, 0)
|
||||
version.setSummary(packageInfo.versionName)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
super.onDestroy()
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the updated username or status to all contacts.
|
||||
*/
|
||||
override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
key match {
|
||||
case KeyUserName | KeyUserStatus =>
|
||||
val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, ""))
|
||||
activity.database.get.getContacts.foreach(c => activity.service.get.sendTo(c.address, ui))
|
||||
case KeyAddresses =>
|
||||
val intent = new Intent(getActivity, classOf[ChatService])
|
||||
intent.setAction(ChatService.ActionNetworkChanged)
|
||||
getActivity.startService(intent)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
import android.app.{AlertDialog, Dialog, DialogFragment}
|
||||
import android.content.{ClipData, ClipboardManager, Context}
|
||||
import android.graphics.{Bitmap, Color}
|
||||
import android.os.Bundle
|
||||
import android.view.View.{OnClickListener, OnLongClickListener}
|
||||
import android.view.{LayoutInflater, View}
|
||||
import android.widget.{ImageView, TextView, Toast}
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.util.IdenticonGenerator
|
||||
|
||||
object UserInfoFragment {
|
||||
val ExtraAddress = "address"
|
||||
val ExtraUserName = "user_name"
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays identicon, username and address for a user.
|
||||
*
|
||||
* Use [[UserInfoFragment#getInstance]] to invoke.
|
||||
*/
|
||||
class UserInfoFragment extends DialogFragment with OnLongClickListener {
|
||||
|
||||
private lazy val address = new Address(getArguments.getString(UserInfoFragment.ExtraAddress))
|
||||
private lazy val userName = getArguments.getString(UserInfoFragment.ExtraUserName)
|
||||
|
||||
override def onCreateDialog(savedInstanceState: Bundle): Dialog = {
|
||||
val view = LayoutInflater.from(getActivity).inflate(R.layout.fragment_identicon, null)
|
||||
|
||||
view.findViewById(R.id.identicon)
|
||||
.asInstanceOf[ImageView]
|
||||
.setImageBitmap(IdenticonGenerator.generate(address, (150, 150), getActivity))
|
||||
val addressTextView = view.findViewById(R.id.address)
|
||||
.asInstanceOf[TextView]
|
||||
addressTextView.setText(getString(R.string.address_colon, address.toString()))
|
||||
addressTextView.setOnLongClickListener(this)
|
||||
addressTextView.setOnClickListener(new OnClickListener {
|
||||
override def onClick(v: View): Unit = onLongClick(v)
|
||||
})
|
||||
|
||||
val matrix = new QRCodeWriter().encode(address.toString(), BarcodeFormat.QR_CODE, 150, 150)
|
||||
view.findViewById(R.id.qr_code)
|
||||
.asInstanceOf[ImageView]
|
||||
.setImageBitmap(renderMatrix(matrix))
|
||||
|
||||
new AlertDialog.Builder(getActivity)
|
||||
.setTitle(userName)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
override def onLongClick(v: View): Boolean = {
|
||||
val cm = getContext.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
|
||||
val clip = ClipData.newPlainText(getContext.getString(R.string.ensichat_user_address), address.toString)
|
||||
cm.setPrimaryClip(clip)
|
||||
Toast.makeText(getContext, R.string.address_copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a [[BitMatrix]] instance into a [[Bitmap]].
|
||||
*/
|
||||
private def renderMatrix(bitMatrix: BitMatrix): Bitmap = {
|
||||
val height = bitMatrix.getHeight
|
||||
val width = bitMatrix.getWidth
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
for (x <- 0 until width) {
|
||||
for (y <- 0 until height) {
|
||||
val color =
|
||||
if (bitMatrix.get(x,y))
|
||||
Color.BLACK
|
||||
else
|
||||
Color.WHITE
|
||||
bmp.setPixel(x, y, color)
|
||||
}
|
||||
}
|
||||
bmp
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.nutomic.ensichat.service
|
||||
|
||||
import android.content.{BroadcastReceiver, Context, Intent}
|
||||
import android.preference.PreferenceManager
|
||||
|
||||
/**
|
||||
* Starts [[ChatService]] on boot if preference is enabled.
|
||||
*/
|
||||
class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
context.startService(new Intent(context, classOf[ChatService]))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package com.nutomic.ensichat.service
|
||||
|
||||
import android.content.Intent
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import com.nutomic.ensichat.core.interfaces.CallbackInterface
|
||||
import com.nutomic.ensichat.core.ConnectionHandler
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import com.nutomic.ensichat.service.CallbackHandler._
|
||||
|
||||
object CallbackHandler {
|
||||
|
||||
val ActionMessageReceived = "message_received"
|
||||
val ActionConnectionsChanged = "connections_changed"
|
||||
val ActionContactsUpdated = "contacts_updated"
|
||||
|
||||
val ExtraMessage = "extra_message"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives events from [[ConnectionHandler]] and sends them as local broadcasts.
|
||||
*/
|
||||
class CallbackHandler(chatService: ChatService, notificationHandler: NotificationHandler)
|
||||
extends CallbackInterface {
|
||||
|
||||
def onMessageReceived(msg: Message): Unit = {
|
||||
notificationHandler.onMessageReceived(msg)
|
||||
val i = new Intent(ActionMessageReceived)
|
||||
i.putExtra(ExtraMessage, msg)
|
||||
LocalBroadcastManager.getInstance(chatService)
|
||||
.sendBroadcast(i)
|
||||
|
||||
}
|
||||
|
||||
def onConnectionsChanged(): Unit = {
|
||||
val i = new Intent(ActionConnectionsChanged)
|
||||
LocalBroadcastManager.getInstance(chatService)
|
||||
.sendBroadcast(i)
|
||||
notificationHandler
|
||||
.updatePersistentNotification(chatService.getConnectionHandler.connections().size)
|
||||
}
|
||||
|
||||
def onContactsUpdated(): Unit = {
|
||||
val i = new Intent(ActionContactsUpdated)
|
||||
LocalBroadcastManager.getInstance(chatService)
|
||||
.sendBroadcast(i)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package com.nutomic.ensichat.service
|
||||
|
||||
import java.io.File
|
||||
|
||||
import android.app.Service
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.{Context, Intent, IntentFilter}
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Handler
|
||||
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
||||
import com.nutomic.ensichat.core.interfaces.TransmissionInterface
|
||||
import com.nutomic.ensichat.core.util.{Crypto, Database}
|
||||
import com.nutomic.ensichat.core.ConnectionHandler
|
||||
import com.nutomic.ensichat.util.{NetworkChangedReceiver, SettingsWrapper}
|
||||
|
||||
object ChatService {
|
||||
|
||||
case class Binder(service: ChatService) extends android.os.Binder
|
||||
|
||||
private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
|
||||
def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
|
||||
|
||||
val ActionNetworkChanged = "network_changed"
|
||||
|
||||
}
|
||||
|
||||
class ChatService extends Service {
|
||||
|
||||
private lazy val binder = new ChatService.Binder(this)
|
||||
|
||||
private lazy val notificationHandler = new NotificationHandler(this)
|
||||
|
||||
private val callbackHandler = new CallbackHandler(this, notificationHandler)
|
||||
|
||||
private def settingsWrapper = new SettingsWrapper(this)
|
||||
|
||||
lazy val database = new Database(getDatabasePath("database"), settingsWrapper, callbackHandler)
|
||||
|
||||
private lazy val connectionHandler =
|
||||
new ConnectionHandler(settingsWrapper, database, callbackHandler, ChatService.newCrypto(this))
|
||||
|
||||
private val networkReceiver = new NetworkChangedReceiver()
|
||||
|
||||
override def onBind(intent: Intent) = binder
|
||||
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
|
||||
Option(intent).foreach { i =>
|
||||
if (i.getAction == ChatService.ActionNetworkChanged)
|
||||
connectionHandler.internetConnectionChanged()
|
||||
}
|
||||
|
||||
Service.START_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates keys and starts Bluetooth interface.
|
||||
*/
|
||||
override def onCreate(): Unit = {
|
||||
super.onCreate()
|
||||
notificationHandler.updatePersistentNotification(getConnectionHandler.connections().size)
|
||||
var additionalInterfaces = Set[TransmissionInterface]()
|
||||
if (Option(BluetoothAdapter.getDefaultAdapter).isDefined)
|
||||
additionalInterfaces += new BluetoothInterface(this, new Handler(), connectionHandler)
|
||||
|
||||
connectionHandler.start(additionalInterfaces)
|
||||
registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
notificationHandler.stopPersistentNotification()
|
||||
connectionHandler.stop()
|
||||
unregisterReceiver(networkReceiver)
|
||||
}
|
||||
|
||||
def getConnectionHandler = connectionHandler
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package com.nutomic.ensichat.service
|
||||
|
||||
import android.app.{Notification, NotificationManager, PendingIntent}
|
||||
import android.content.{Context, Intent}
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.MainActivity
|
||||
import com.nutomic.ensichat.core.messages.body.Text
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import com.nutomic.ensichat.service.NotificationHandler._
|
||||
|
||||
object NotificationHandler {
|
||||
|
||||
private val NotificationIdRunning = 1
|
||||
|
||||
private val NotificationIdNewMessage = 2
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays notifications for new messages and while the app is running.
|
||||
*/
|
||||
class NotificationHandler(context: Context) {
|
||||
|
||||
private lazy val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
.asInstanceOf[NotificationManager]
|
||||
|
||||
private var persistentNotificationShutdown = false
|
||||
|
||||
def updatePersistentNotification(connections: Int): Unit = {
|
||||
if (persistentNotificationShutdown)
|
||||
return
|
||||
|
||||
val intent = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
|
||||
val info = context.getResources
|
||||
.getQuantityString(R.plurals.notification_connections, connections, connections.toString)
|
||||
|
||||
val notification = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(context.getString(R.string.app_name))
|
||||
.setContentText(info)
|
||||
.setContentIntent(intent)
|
||||
.setOngoing(true)
|
||||
.setPriority(Notification.PRIORITY_MIN)
|
||||
.build()
|
||||
notificationManager.notify(NotificationIdRunning, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the persistent notification.
|
||||
*
|
||||
* After calling this method, [[updatePersistentNotification()]] will have no effect.
|
||||
*/
|
||||
def stopPersistentNotification() = {
|
||||
persistentNotificationShutdown = true
|
||||
notificationManager.cancel(NotificationIdRunning)
|
||||
}
|
||||
|
||||
def onMessageReceived(msg: Message): Unit = msg.body match {
|
||||
case text: Text =>
|
||||
if (msg.header.origin == ChatService.newCrypto(context).localAddress)
|
||||
return
|
||||
|
||||
val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
|
||||
val notification = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(context.getString(R.string.notification_message))
|
||||
.setContentText(text.text)
|
||||
.setDefaults(defaults())
|
||||
.setContentIntent(pi)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(NotificationIdNewMessage, notification)
|
||||
case _ =>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default notification options that should be used.
|
||||
*/
|
||||
private def defaults(): Int = {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (sp.getBoolean(SettingsInterface.KeyNotificationSoundsOn, SettingsInterface.DefaultNotificationSoundsOn))
|
||||
Notification.DEFAULT_ALL
|
||||
else
|
||||
Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,7 @@ package com.nutomic.ensichat.util
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap.Config
|
||||
import android.graphics.{Bitmap, Canvas, Color}
|
||||
import com.nutomic.ensichat.protocol.Address
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
|
||||
/**
|
||||
* Calculates a unique identicon for the given hash.
|
|
@ -0,0 +1,31 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import android.content.{BroadcastReceiver, Context, Intent}
|
||||
import android.net.ConnectivityManager
|
||||
import com.nutomic.ensichat.service.ChatService
|
||||
|
||||
/**
|
||||
* Forwards network changed intents to [[ChatService]].
|
||||
*
|
||||
* HACK: Because [[ConnectivityManager.CONNECTIVITY_ACTION]] is a sticky intent, and we register it
|
||||
* from Scala, an intent is sent as soon as the receiver is registered. As a workaround, we
|
||||
* ignore the first intent received.
|
||||
* Alternatively, we can register the receiver in the manifest, but that will start the
|
||||
* service (so it only works if the service runs permanently, with no exit).
|
||||
*/
|
||||
class NetworkChangedReceiver extends BroadcastReceiver {
|
||||
|
||||
private var isFirstIntent = true
|
||||
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
if (isFirstIntent) {
|
||||
isFirstIntent = false
|
||||
return
|
||||
}
|
||||
|
||||
val intent = new Intent(context, classOf[ChatService])
|
||||
intent.setAction(ChatService.ActionNetworkChanged)
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||
|
||||
class SettingsWrapper(context: Context) extends SettingsInterface {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
override def get[T](key: String, default: T): T = default match {
|
||||
case s: String => prefs.getString(key, s).asInstanceOf[T]
|
||||
case i: Int => prefs.getInt(key, i).asInstanceOf[T]
|
||||
case l: Long => prefs.getLong(key, l).asInstanceOf[T]
|
||||
}
|
||||
|
||||
override def put[T](key: String, value: T): Unit = value match {
|
||||
case s: String => prefs.edit().putString(key, s).apply()
|
||||
case i: Int => prefs.edit().putInt(key, i).apply()
|
||||
case l: Long => prefs.edit().putLong(key, l).apply()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.nutomic.ensichat.views
|
||||
|
||||
import java.text.DateFormat
|
||||
|
||||
import android.content.Context
|
||||
import com.mobsandgeeks.adapters.{Sectionizer, SimpleSectionAdapter}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
object DatesAdapter {
|
||||
|
||||
private val Sectionizer = new Sectionizer[Message]() {
|
||||
override def getSectionTitleForItem(item: Message): String = {
|
||||
DateFormat
|
||||
.getDateInstance(DateFormat.MEDIUM)
|
||||
.format(item.header.time.get.toDate)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps [[MessagesAdapter]] and shows date between messages.
|
||||
*/
|
||||
class DatesAdapter(context: Context, messagesAdapter: MessagesAdapter)
|
||||
extends SimpleSectionAdapter[Message](context, messagesAdapter, R.layout.item_date, R.id.date,
|
||||
DatesAdapter.Sectionizer) {
|
||||
|
||||
def replaceItems(items: Seq[Message]): Unit = {
|
||||
messagesAdapter.clear()
|
||||
messagesAdapter.addAll(items.asJava)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,27 @@
|
|||
package com.nutomic.ensichat.views
|
||||
|
||||
import java.text.DateFormat
|
||||
import java.util
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.view._
|
||||
import android.widget._
|
||||
import com.mobsandgeeks.adapters.{InstantCursorAdapter, SimpleSectionAdapter, ViewHandler}
|
||||
import com.mobsandgeeks.adapters.{InstantAdapter, SimpleSectionAdapter, ViewHandler}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.protocol.body.Text
|
||||
import com.nutomic.ensichat.protocol.{Address, Message}
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.core.messages.body.Text
|
||||
import com.nutomic.ensichat.core.messages.Message
|
||||
import com.nutomic.ensichat.core.routing.Address
|
||||
import com.nutomic.ensichat.views.MessagesAdapter._
|
||||
|
||||
object MessagesAdapter {
|
||||
|
||||
private def itemsAsMutableList(items: Seq[Message]): util.List[Message] = {
|
||||
val list = new util.ArrayList[Message]()
|
||||
items.foreach(list.add)
|
||||
list
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays [[Message]]s in ListView.
|
||||
|
@ -18,8 +29,9 @@ import com.nutomic.ensichat.util.Database
|
|||
* We just use the instant adapter for compatibility with [[SimpleSectionAdapter]], but don't use
|
||||
* the annotations (as it breaks separation of presentation and content).
|
||||
*/
|
||||
class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address) extends
|
||||
InstantCursorAdapter[Message](context, R.layout.item_message, classOf[Message], cursor) {
|
||||
class MessagesAdapter(context: Context, items: Seq[Message], remoteAddress: Address) extends
|
||||
InstantAdapter[Message](context, R.layout.item_message, classOf[Message],
|
||||
itemsAsMutableList(items)) {
|
||||
|
||||
private val MessagePaddingLarge = 50
|
||||
private val MessagePaddingSmall = 10
|
||||
|
@ -35,7 +47,7 @@ class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address)
|
|||
text.setText(msg.body.asInstanceOf[Text].text)
|
||||
val formattedDate = DateFormat
|
||||
.getTimeInstance(DateFormat.SHORT)
|
||||
.format(msg.header.time.get)
|
||||
.format(msg.header.time.get.toDate)
|
||||
time.setText(formattedDate)
|
||||
|
||||
val paddingLarge = (MessagePaddingLarge * context.getResources.getDisplayMetrics.density).toInt
|
||||
|
@ -52,6 +64,4 @@ class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address)
|
|||
}
|
||||
})
|
||||
|
||||
override def getInstance (cursor: Cursor) = Database.messageFromCursor(cursor)
|
||||
|
||||
}
|
|
@ -7,8 +7,8 @@ import android.view.View.OnClickListener
|
|||
import android.view.{LayoutInflater, View, ViewGroup}
|
||||
import android.widget.{ArrayAdapter, ImageView, TextView}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.fragments.IdenticonFragment
|
||||
import com.nutomic.ensichat.protocol.{Crypto, User}
|
||||
import com.nutomic.ensichat.core.util.User
|
||||
import com.nutomic.ensichat.fragments.UserInfoFragment
|
||||
import com.nutomic.ensichat.util.IdenticonGenerator
|
||||
|
||||
/**
|
||||
|
@ -38,12 +38,12 @@ class UsersAdapter(activity: Activity) extends ArrayAdapter[User](activity, 0) w
|
|||
view
|
||||
}
|
||||
|
||||
override def onClick (v: View): Unit = {
|
||||
override def onClick(v: View): Unit = {
|
||||
val user = v.getTag.asInstanceOf[User]
|
||||
val fragment = new IdenticonFragment()
|
||||
val fragment = new UserInfoFragment()
|
||||
val bundle = new Bundle()
|
||||
bundle.putString(IdenticonFragment.ExtraAddress, user.address.toString)
|
||||
bundle.putString(IdenticonFragment.ExtraUserName, user.name)
|
||||
bundle.putString(UserInfoFragment.ExtraAddress, user.address.toString)
|
||||
bundle.putString(UserInfoFragment.ExtraUserName, user.name)
|
||||
fragment.setArguments(bundle)
|
||||
fragment.show(activity.getFragmentManager, "dialog")
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content._
|
||||
import android.test.ActivityUnitTestCase
|
||||
import junit.framework.Assert
|
||||
|
||||
class MainActivityTest extends ActivityUnitTestCase[MainActivity](classOf[MainActivity]) {
|
||||
|
||||
var lastIntent: Intent = _
|
||||
|
||||
class ActivityContextWrapper(context: Context) extends ContextWrapper(context) {
|
||||
override def startService(service: Intent): ComponentName = {
|
||||
lastIntent = service
|
||||
null
|
||||
}
|
||||
|
||||
override def stopService(name: Intent): Boolean = {
|
||||
lastIntent = name
|
||||
true
|
||||
}
|
||||
|
||||
override def bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean = false
|
||||
|
||||
override def unbindService(conn: ServiceConnection): Unit = {}
|
||||
}
|
||||
|
||||
override def setUp(): Unit = {
|
||||
setActivityContext(new ActivityContextWrapper(getInstrumentation.getTargetContext))
|
||||
startActivity(new Intent(), null, null)
|
||||
}
|
||||
|
||||
def testRequestBluetoothDiscoverable(): Unit = {
|
||||
val intent: Intent = getStartedActivityIntent
|
||||
Assert.assertEquals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE, intent.getAction)
|
||||
Assert.assertEquals(0, intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, -1))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import junit.framework.Assert._
|
||||
|
||||
class CryptoTest extends AndroidTestCase {
|
||||
|
||||
private lazy val crypto: Crypto = new Crypto(getContext)
|
||||
|
||||
override def setUp(): Unit = {
|
||||
super.setUp()
|
||||
if (!crypto.localKeysExist) {
|
||||
crypto.generateLocalKeys()
|
||||
}
|
||||
}
|
||||
|
||||
def testSignVerify(): Unit = {
|
||||
MessageTest.messages.foreach { m =>
|
||||
val signed = crypto.sign(m)
|
||||
assertTrue(crypto.verify(signed, crypto.getLocalPublicKey))
|
||||
assertEquals(m.header, signed.header)
|
||||
assertEquals(m.body, signed.body)
|
||||
}
|
||||
}
|
||||
|
||||
def testEncryptDecrypt(): Unit = {
|
||||
MessageTest.messages.foreach{ m =>
|
||||
val encrypted = crypto.encrypt(crypto.sign(m), crypto.getLocalPublicKey)
|
||||
val decrypted = crypto.decrypt(encrypted)
|
||||
assertEquals(m.body, decrypted.body)
|
||||
assertEquals(m.header, encrypted.header)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.MessageTest._
|
||||
import com.nutomic.ensichat.protocol.body.{ConnectionInfo, ConnectionInfoTest, Text}
|
||||
import com.nutomic.ensichat.protocol.header.ContentHeaderTest._
|
||||
import com.nutomic.ensichat.protocol.header.MessageHeader
|
||||
import junit.framework.Assert._
|
||||
|
||||
import scala.collection.immutable.TreeSet
|
||||
|
||||
object MessageTest {
|
||||
|
||||
val m1 = new Message(h1, new Text("first"))
|
||||
|
||||
val m2 = new Message(h2, new Text("second"))
|
||||
|
||||
val m3 = new Message(h3, new Text("third"))
|
||||
|
||||
val messages = Set(m1, m2, m3)
|
||||
|
||||
}
|
||||
|
||||
class MessageTest extends AndroidTestCase {
|
||||
|
||||
private lazy val crypto: Crypto = new Crypto(getContext)
|
||||
|
||||
override def setUp(): Unit = {
|
||||
super.setUp()
|
||||
if (!crypto.localKeysExist) {
|
||||
crypto.generateLocalKeys()
|
||||
}
|
||||
}
|
||||
|
||||
def testOrder(): Unit = {
|
||||
var messages = new TreeSet[Message]()(Message.Ordering)
|
||||
messages += m1
|
||||
messages += m2
|
||||
assertEquals(m1, messages.firstKey)
|
||||
|
||||
messages = new TreeSet[Message]()(Message.Ordering)
|
||||
messages += m2
|
||||
messages += m3
|
||||
assertEquals(m2, messages.firstKey)
|
||||
}
|
||||
|
||||
def testSerializeSigned(): Unit = {
|
||||
val header = new MessageHeader(ConnectionInfo.Type, AddressTest.a4, AddressTest.a2, 0)
|
||||
val m = new Message(header, ConnectionInfoTest.generateCi(getContext))
|
||||
|
||||
val signed = crypto.sign(m)
|
||||
val bytes = signed.write
|
||||
val read = Message.read(new ByteArrayInputStream(bytes))
|
||||
|
||||
assertEquals(signed, read)
|
||||
assertTrue(crypto.verify(read, crypto.getLocalPublicKey))
|
||||
}
|
||||
|
||||
def testSerializeEncrypted(): Unit = {
|
||||
MessageTest.messages.foreach{ m =>
|
||||
val signed = crypto.sign(m)
|
||||
val encrypted = crypto.encrypt(signed, crypto.getLocalPublicKey)
|
||||
val bytes = encrypted.write
|
||||
|
||||
val read = Message.read(new ByteArrayInputStream(bytes))
|
||||
assertEquals(encrypted.crypto, read.crypto)
|
||||
val decrypted = crypto.decrypt(read)
|
||||
assertEquals(m.header, decrypted.header)
|
||||
assertEquals(m.body, decrypted.body)
|
||||
assertTrue(crypto.verify(decrypted, crypto.getLocalPublicKey))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
|
||||
import java.util.{Date, GregorianCalendar}
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.body.{Text, UserInfo}
|
||||
import com.nutomic.ensichat.protocol.header.ContentHeader
|
||||
import junit.framework.Assert._
|
||||
|
||||
class RouterTest extends AndroidTestCase {
|
||||
|
||||
private def neighbors() = Set[Address](AddressTest.a1, AddressTest.a2, AddressTest.a3)
|
||||
|
||||
private val msg = generateMessage(AddressTest.a1, AddressTest.a4, 1)
|
||||
|
||||
/**
|
||||
* Messages should be sent to all neighbors.
|
||||
*/
|
||||
def testFlooding(): Unit = {
|
||||
var sentTo = Set[Address]()
|
||||
val router: Router = new Router(neighbors,
|
||||
(a, m) => {
|
||||
sentTo += a
|
||||
})
|
||||
|
||||
router.onReceive(msg)
|
||||
assertEquals(neighbors(), sentTo)
|
||||
}
|
||||
|
||||
def testMessageSame(): Unit = {
|
||||
val router: Router = new Router(neighbors,
|
||||
(a, m) => {
|
||||
assertEquals(msg.header.origin, m.header.origin)
|
||||
assertEquals(msg.header.target, m.header.target)
|
||||
assertEquals(msg.header.seqNum, m.header.seqNum)
|
||||
assertEquals(msg.header.protocolType, m.header.protocolType)
|
||||
assertEquals(msg.header.hopCount + 1, m.header.hopCount)
|
||||
assertEquals(msg.header.hopLimit, m.header.hopLimit)
|
||||
assertEquals(msg.body, m.body)
|
||||
assertEquals(msg.crypto, m.crypto)
|
||||
})
|
||||
router.onReceive(msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages from different senders with the same sequence number should be forwarded.
|
||||
*/
|
||||
def testDifferentSenders(): Unit = {
|
||||
var sentTo = Set[Address]()
|
||||
val router: Router = new Router(neighbors, (a, m) => sentTo += a)
|
||||
|
||||
router.onReceive(msg)
|
||||
assertEquals(neighbors(), sentTo)
|
||||
|
||||
sentTo = Set[Address]()
|
||||
router.onReceive(generateMessage(AddressTest.a2, AddressTest.a4, 1))
|
||||
assertEquals(neighbors(), sentTo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Messages from the same sender with the same sequence number should be ignored.
|
||||
*/
|
||||
def testIgnores(): Unit = {
|
||||
var sentTo = Set[Address]()
|
||||
val router: Router = new Router(neighbors, (a, m) => sentTo += a)
|
||||
|
||||
router.onReceive(msg)
|
||||
assertEquals(neighbors(), sentTo)
|
||||
|
||||
sentTo = Set[Address]()
|
||||
router.onReceive(generateMessage(AddressTest.a1, AddressTest.a2, 1))
|
||||
assertTrue(sentTo.isEmpty)
|
||||
}
|
||||
|
||||
def testDiscardOldIgnores(): Unit = {
|
||||
def test(first: Int, second: Int) {
|
||||
var sentTo = Set[Address]()
|
||||
val router: Router = new Router(neighbors, (a, m) => sentTo += a)
|
||||
router.onReceive(generateMessage(AddressTest.a1, AddressTest.a3, first))
|
||||
router.onReceive(generateMessage(AddressTest.a1, AddressTest.a3, second))
|
||||
|
||||
sentTo = Set[Address]()
|
||||
router.onReceive(generateMessage(AddressTest.a1, AddressTest.a3, first))
|
||||
assertEquals(neighbors(), sentTo)
|
||||
}
|
||||
|
||||
test(1, ContentHeader.SeqNumRange.last)
|
||||
test(ContentHeader.SeqNumRange.last / 2, ContentHeader.SeqNumRange.last)
|
||||
test(ContentHeader.SeqNumRange.last / 2, 1)
|
||||
}
|
||||
|
||||
def testHopLimit(): Unit = Range(19, 22).foreach { i =>
|
||||
val msg = new Message(
|
||||
new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), i), new Text(""))
|
||||
val router: Router = new Router(neighbors, (a, m) => fail())
|
||||
router.onReceive(msg)
|
||||
}
|
||||
|
||||
private def generateMessage(sender: Address, receiver: Address, seqNum: Int): Message = {
|
||||
val header = new ContentHeader(sender, receiver, seqNum, UserInfo.Type, Some(5),
|
||||
Some(new GregorianCalendar(2014, 6, 10).getTime))
|
||||
new Message(header, new UserInfo("", ""))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol
|
||||
|
||||
object UserTest {
|
||||
|
||||
val u1 = new User(AddressTest.a1, "one", "s1")
|
||||
|
||||
val u2 = new User(AddressTest.a2, "two", "s2")
|
||||
|
||||
val u3 = new User(AddressTest.a3, "three", "s3")
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol.body
|
||||
|
||||
import android.content.Context
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.Crypto
|
||||
import junit.framework.Assert
|
||||
|
||||
object ConnectionInfoTest {
|
||||
|
||||
def generateCi(context: Context) = {
|
||||
val crypto = new Crypto(context)
|
||||
if (!crypto.localKeysExist)
|
||||
crypto.generateLocalKeys()
|
||||
new ConnectionInfo(crypto.getLocalPublicKey)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ConnectionInfoTest extends AndroidTestCase {
|
||||
|
||||
def testWriteRead(): Unit = {
|
||||
val ci = ConnectionInfoTest.generateCi(getContext)
|
||||
val bytes = ci.write
|
||||
val body = ConnectionInfo.read(bytes)
|
||||
Assert.assertEquals(ci.key, body.asInstanceOf[ConnectionInfo].key)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol.body
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import junit.framework.Assert
|
||||
|
||||
class UserInfoTest extends AndroidTestCase {
|
||||
|
||||
def testWriteRead(): Unit = {
|
||||
val name = new UserInfo("name", "status")
|
||||
val bytes = name.write
|
||||
val body = UserInfo.read(bytes)
|
||||
Assert.assertEquals(name, body.asInstanceOf[UserInfo])
|
||||
}
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol.header
|
||||
|
||||
import java.util.{GregorianCalendar, Date}
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.body.Text
|
||||
import com.nutomic.ensichat.protocol.{Address, AddressTest}
|
||||
import junit.framework.Assert._
|
||||
|
||||
object ContentHeaderTest {
|
||||
|
||||
val h1 = new ContentHeader(AddressTest.a1, AddressTest.a2, 1234,
|
||||
Text.Type, Some(123), Some(new GregorianCalendar(1970, 1, 1).getTime), 5)
|
||||
|
||||
val h2 = new ContentHeader(AddressTest.a1, AddressTest.a3,
|
||||
30000, Text.Type, Some(8765), Some(new GregorianCalendar(2014, 6, 10).getTime), 20)
|
||||
|
||||
val h3 = new ContentHeader(AddressTest.a4, AddressTest.a2,
|
||||
250, Text.Type, Some(77), Some(new GregorianCalendar(2020, 11, 11).getTime), 123)
|
||||
|
||||
val h4 = new ContentHeader(Address.Null, Address.Broadcast,
|
||||
ContentHeader.SeqNumRange.last, 0, Some(0xffff), Some(new Date(0L)), 0xff)
|
||||
|
||||
val h5 = new ContentHeader(Address.Broadcast, Address.Null,
|
||||
0, 0xff, Some(0), Some(new Date(0xffffffffL)), 0)
|
||||
|
||||
val headers = Set(h1, h2, h3, h4, h5)
|
||||
|
||||
}
|
||||
|
||||
class ContentHeaderTest extends AndroidTestCase {
|
||||
|
||||
def testSerialize(): Unit = {
|
||||
ContentHeaderTest.headers.foreach{h =>
|
||||
val bytes = h.write(0)
|
||||
assertEquals(bytes.length, h.length)
|
||||
val (mh, length) = MessageHeader.read(bytes)
|
||||
val chBytes = bytes.drop(mh.length)
|
||||
val (header, remaining) = ContentHeader.read(mh, chBytes)
|
||||
assertEquals(h, header)
|
||||
assertEquals(0, remaining.length)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package com.nutomic.ensichat.protocol.header
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.header.MessageHeaderTest._
|
||||
import com.nutomic.ensichat.protocol.{Address, AddressTest}
|
||||
import junit.framework.Assert._
|
||||
|
||||
object MessageHeaderTest {
|
||||
|
||||
val h1 = new MessageHeader(ContentHeader.ContentMessageType, AddressTest.a1, AddressTest.a2, 1234,
|
||||
0)
|
||||
|
||||
val h2 = new MessageHeader(ContentHeader.ContentMessageType, Address.Null, Address.Broadcast,
|
||||
ContentHeader.SeqNumRange.last, 0xff)
|
||||
|
||||
val h3 = new MessageHeader(ContentHeader.ContentMessageType, Address.Broadcast, Address.Null, 0)
|
||||
|
||||
val headers = Set(h1, h2, h3)
|
||||
|
||||
}
|
||||
|
||||
class MessageHeaderTest extends AndroidTestCase {
|
||||
|
||||
def testSerialize(): Unit = {
|
||||
headers.foreach{h =>
|
||||
val bytes = h.write(0)
|
||||
val (header, length) = MessageHeader.read(bytes)
|
||||
assertEquals(h, header)
|
||||
assertEquals(MessageHeader.Length, length)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
import android.content._
|
||||
import android.database.DatabaseErrorHandler
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.MessageTest._
|
||||
import com.nutomic.ensichat.protocol.body.CryptoData
|
||||
import com.nutomic.ensichat.protocol.header.{ContentHeader, ContentHeaderTest}
|
||||
import com.nutomic.ensichat.protocol.header.ContentHeaderTest._
|
||||
import com.nutomic.ensichat.protocol.{MessageTest, UserTest}
|
||||
import junit.framework.Assert._
|
||||
|
||||
object DatabaseTest {
|
||||
|
||||
/**
|
||||
* Provides a temporary database file that can be deleted easily.
|
||||
*/
|
||||
class DatabaseContext(context: Context) extends ContextWrapper(context) {
|
||||
private val dbFile = "database-test.db"
|
||||
override def openOrCreateDatabase(file: String, mode: Int, factory:
|
||||
SQLiteDatabase.CursorFactory, errorHandler: DatabaseErrorHandler): SQLiteDatabase = {
|
||||
context.openOrCreateDatabase(dbFile, mode, factory, errorHandler)
|
||||
}
|
||||
def deleteDbFile() = context.deleteDatabase(dbFile)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DatabaseTest extends AndroidTestCase {
|
||||
|
||||
private lazy val context = new DatabaseTest.DatabaseContext(getContext)
|
||||
|
||||
private lazy val database = new Database(context)
|
||||
|
||||
override def setUp(): Unit = {
|
||||
super.setUp()
|
||||
database.onMessageReceived(m1)
|
||||
database.onMessageReceived(m2)
|
||||
database.onMessageReceived(m3)
|
||||
}
|
||||
|
||||
override def tearDown(): Unit = {
|
||||
super.tearDown()
|
||||
database.close()
|
||||
context.deleteDbFile()
|
||||
}
|
||||
|
||||
def testMessageCount(): Unit = {
|
||||
val msg1 = database.getMessages(m1.header.origin, 1)
|
||||
assertEquals(1, msg1.size)
|
||||
|
||||
val msg2 = database.getMessages(m1.header.origin, 3)
|
||||
assertEquals(2, msg2.size)
|
||||
}
|
||||
|
||||
def testMessageOrder(): Unit = {
|
||||
val msg = database.getMessages(m1.header.target, 1)
|
||||
assertTrue(msg.contains(m1))
|
||||
}
|
||||
|
||||
def testMessageSelect(): Unit = {
|
||||
val msg = database.getMessages(m1.header.target, 2)
|
||||
assertTrue(msg.contains(m1))
|
||||
assertTrue(msg.contains(m3))
|
||||
}
|
||||
|
||||
def testMessageFields(): Unit = {
|
||||
val msg = database.getMessages(m2.header.target, 1).firstKey
|
||||
val header = msg.header.asInstanceOf[ContentHeader]
|
||||
|
||||
assertEquals(h2.origin, header.origin)
|
||||
assertEquals(h2.target, header.target)
|
||||
assertEquals(-1, msg.header.seqNum)
|
||||
assertEquals(h2.contentType, header.contentType)
|
||||
assertEquals(h2.messageId, header.messageId)
|
||||
assertEquals(h2.time, header.time)
|
||||
assertEquals(new CryptoData(None, None), msg.crypto)
|
||||
assertEquals(m2.body, msg.body)
|
||||
}
|
||||
|
||||
def testAddContact(): Unit = {
|
||||
database.addContact(UserTest.u1)
|
||||
val contacts = database.getContacts
|
||||
assertEquals(1, contacts.size)
|
||||
assertEquals(Option(UserTest.u1), database.getContact(UserTest.u1.address))
|
||||
}
|
||||
|
||||
def testAddContactCallback(): Unit = {
|
||||
val latch = new CountDownLatch(1)
|
||||
val lbm = LocalBroadcastManager.getInstance(context)
|
||||
val receiver = new BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = latch.countDown()
|
||||
}
|
||||
lbm.registerReceiver(receiver, new IntentFilter(Database.ActionContactsUpdated))
|
||||
database.addContact(UserTest.u1)
|
||||
latch.await()
|
||||
lbm.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
def testGetContact(): Unit = {
|
||||
database.addContact(UserTest.u2)
|
||||
assertTrue(database.getContact(UserTest.u1.address).isEmpty)
|
||||
val c = database.getContact(UserTest.u2.address)
|
||||
assertTrue(c.nonEmpty)
|
||||
assertEquals(Option(UserTest.u2), c)
|
||||
}
|
||||
|
||||
}
|
Before Width: | Height: | Size: 616 B |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 469 B |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 19 KiB |
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_contacts_found"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="25dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/identicon"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_marginBottom="25dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources translatable="false">
|
||||
|
||||
<string name="default_user_status">Let\'s chat!</string>
|
||||
|
||||
<string name="default_scan_interval">15</string>
|
||||
|
||||
<bool name="default_notification_sounds">true</bool>
|
||||
|
||||
<string name="default_max_connections">1000000</string>
|
||||
|
||||
</resources>
|
|
@ -1,94 +0,0 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import android.app.AlertDialog.Builder
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.content._
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.view._
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget._
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.protocol.ChatService
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.views.UsersAdapter
|
||||
|
||||
/**
|
||||
* Lists all nearby, connected devices and allows adding them to be added as contacts.
|
||||
*/
|
||||
class AddContactsActivity extends EnsichatActivity with OnItemClickListener {
|
||||
|
||||
private val Tag = "AddContactsActivity"
|
||||
|
||||
private lazy val database = new Database(this)
|
||||
|
||||
private lazy val adapter = new UsersAdapter(this)
|
||||
|
||||
/**
|
||||
* Initializes layout, registers connection and message listeners.
|
||||
*/
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
getSupportActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
setContentView(R.layout.activity_add_contacts)
|
||||
val list = findViewById(android.R.id.list).asInstanceOf[ListView]
|
||||
list.setAdapter(adapter)
|
||||
list.setOnItemClickListener(this)
|
||||
list.setEmptyView(findViewById(android.R.id.empty))
|
||||
|
||||
val filter = new IntentFilter()
|
||||
filter.addAction(ChatService.ActionConnectionsChanged)
|
||||
filter.addAction(Database.ActionContactsUpdated)
|
||||
LocalBroadcastManager.getInstance(this)
|
||||
.registerReceiver(onContactsUpdatedReceiver, filter)
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
super.onDestroy()
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(onContactsUpdatedReceiver)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates adding the device as contact if it hasn't been added yet.
|
||||
*/
|
||||
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
|
||||
val contact = adapter.getItem(position)
|
||||
new Builder(this)
|
||||
.setMessage(getString(R.string.dialog_add_contact, contact.name))
|
||||
.setPositiveButton(android.R.string.yes, new OnClickListener {
|
||||
override def onClick(dialog: DialogInterface, which: Int): Unit = {
|
||||
database.addContact(contact)
|
||||
Toast.makeText(AddContactsActivity.this, R.string.toast_contact_added, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||
case android.R.id.home =>
|
||||
NavUtils.navigateUpFromSameTask(this)
|
||||
true
|
||||
case _ =>
|
||||
super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches connections and displays them (excluding contacts).
|
||||
*/
|
||||
private val onContactsUpdatedReceiver = new BroadcastReceiver() {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
runOnUiThread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
adapter.clear()
|
||||
(service.connections().map(a => service.getUser(a)) -- database.getContacts)
|
||||
.foreach(adapter.add)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
import java.io.File
|
||||
|
||||
import android.app.ListFragment
|
||||
import android.content.{IntentFilter, Context, BroadcastReceiver, Intent}
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.view._
|
||||
import android.widget.ListView
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsichatActivity, MainActivity, SettingsActivity}
|
||||
import com.nutomic.ensichat.protocol.{Crypto, ChatService}
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.views.UsersAdapter
|
||||
import scala.collection.JavaConversions._
|
||||
|
||||
/**
|
||||
* Lists all nearby, connected devices.
|
||||
*/
|
||||
class ContactsFragment extends ListFragment {
|
||||
|
||||
private lazy val adapter = new UsersAdapter(getActivity)
|
||||
|
||||
private lazy val database = new Database(getActivity)
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setListAdapter(adapter)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
getActivity.asInstanceOf[EnsichatActivity].runOnServiceConnected(() => {
|
||||
database.getContacts.foreach(adapter.add)
|
||||
})
|
||||
LocalBroadcastManager.getInstance(getActivity)
|
||||
.registerReceiver(onContactsUpdatedListener, new IntentFilter(Database.ActionContactsUpdated))
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
super.onDestroy()
|
||||
LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(onContactsUpdatedListener)
|
||||
}
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
savedInstanceState: Bundle): View =
|
||||
inflater.inflate(R.layout.fragment_contacts, container, false)
|
||||
|
||||
override def onCreateOptionsMenu(menu: Menu, inflater: MenuInflater): Unit = {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.main, menu)
|
||||
}
|
||||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||
case R.id.add_contact =>
|
||||
startActivity(new Intent(getActivity, classOf[AddContactsActivity]))
|
||||
true
|
||||
case R.id.share_app =>
|
||||
val pm = getActivity.getPackageManager
|
||||
val ai = pm.getInstalledApplications(0).find(_.sourceDir.contains(getActivity.getPackageName))
|
||||
val intent = new Intent()
|
||||
intent.setAction(Intent.ACTION_SEND)
|
||||
intent.setType("*/*")
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(ai.get.sourceDir)))
|
||||
startActivity(intent)
|
||||
true
|
||||
case R.id.my_address =>
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(getActivity)
|
||||
val fragment = new IdenticonFragment()
|
||||
val bundle = new Bundle()
|
||||
bundle.putString(
|
||||
IdenticonFragment.ExtraAddress, new Crypto(getActivity).localAddress.toString)
|
||||
bundle.putString(
|
||||
IdenticonFragment.ExtraUserName, prefs.getString(SettingsFragment.KeyUserName, ""))
|
||||
fragment.setArguments(bundle)
|
||||
fragment.show(getFragmentManager, "dialog")
|
||||
true
|
||||
case R.id.settings =>
|
||||
startActivity(new Intent(getActivity, classOf[SettingsActivity]))
|
||||
true
|
||||
case R.id.exit =>
|
||||
getActivity.stopService(new Intent(getActivity, classOf[ChatService]))
|
||||
getActivity.finish()
|
||||
true
|
||||
case _ =>
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a chat with the clicked device.
|
||||
*/
|
||||
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit =
|
||||
getActivity.asInstanceOf[MainActivity].openChat(adapter.getItem(position).address)
|
||||
|
||||
private val onContactsUpdatedListener = new BroadcastReceiver() {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
getActivity.runOnUiThread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
adapter.clear()
|
||||
database.getContacts.foreach(adapter.add)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|