Compare commits
1 commit
master
...
send-bitco
Author | SHA1 | Date | |
---|---|---|---|
c350ac12e0 |
3
.gitignore
vendored
|
@ -4,5 +4,4 @@
|
|||
/build
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
*.apk
|
||||
*.ipr
|
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "buildSrc"]
|
||||
path = buildSrc
|
||||
url = https://github.com/xelnaga/gradle-android-scala-plugin.git
|
32
.travis.yml
|
@ -1,32 +0,0 @@
|
|||
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
|
147
PROTOCOL.md
|
@ -26,9 +26,6 @@ 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
|
||||
------
|
||||
|
@ -43,15 +40,22 @@ private key, and the result written to the 'Encryption Data' part.
|
|||
|
||||
Routing
|
||||
-------
|
||||
The routing protocol is based on
|
||||
[AODVv2](https://datatracker.ietf.org/doc/draft-ietf-manet-aodvv2/),
|
||||
with various features left out.
|
||||
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.
|
||||
|
||||
TODO: Add Documentation for routing protocol.
|
||||
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.
|
||||
|
||||
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
|
||||
--------
|
||||
|
@ -80,12 +84,14 @@ 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.
|
||||
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.
|
||||
|
||||
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 | Tokens | Hop Count |
|
||||
| Version | Protocol-Type | Hop Limit | Hop Count |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
@ -111,8 +117,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.
|
||||
|
||||
Tokens is the number of times this message should be copied to
|
||||
different relays.
|
||||
Hop Limit SHOULD be set to `MAX_HOP_COUNT` on message creation, and
|
||||
MUST NOT be changed by a forwarding node.
|
||||
|
||||
Hop Count specifies the number of nodes a message may pass. When
|
||||
creating a package, it is initialized to 0. Whenever a node forwards
|
||||
|
@ -214,100 +220,6 @@ 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
|
||||
----------------
|
||||
|
||||
|
@ -354,13 +266,28 @@ 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)
|
||||
### InitiatePayment (Type = 5)
|
||||
|
||||
Requests PaymentRequest 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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Message ID |
|
||||
| Reserved |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Confirms that a previous content message has been received by the
|
||||
target node. Message ID is the ID of that message.
|
||||
|
||||
### PaymentInformation (Type = 6)
|
||||
|
||||
Contains Bitcoin payment info.
|
||||
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Payment Request Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Payment Request (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Payment Request is a protobuf-formatted Bitcoin payment message.
|
||||
|
|
44
README.md
|
@ -1,24 +1,11 @@
|
|||
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
|
||||
========
|
||||
|
||||
[![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)
|
||||
[![BitCoin donate button](https://img.shields.io/badge/bitcoin-donate-yellow.svg)](https://blockchain.info/address/1DmU6QVGSKXGXJU1bqmmStPDNsNnYoMJB4)
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
<img src="graphics/screenshot_phone_1.png" alt="screenshot 1" width="200" />
|
||||
<img src="graphics/screenshot_phone_2.png" alt="screenshot 2" width="200" />
|
||||
|
@ -26,31 +13,14 @@ read the [protocol definition](PROTOCOL.md).
|
|||
|
||||
[![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 setup a development environment, just install [Android Studio](https://developer.android.com/sdk/)
|
||||
and import the project.
|
||||
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
|
||||
|
||||
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).
|
||||
To create a release apk, run `./gradlew assembleRelRelease`.
|
||||
|
||||
License
|
||||
-------
|
||||
The project is licensed under the [MPLv2](LICENSE).
|
||||
|
||||
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/).
|
||||
All code is licensed under the [GPL](LICENSE), v3 or later.
|
||||
|
|
Before Width: | Height: | Size: 127 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 391 B |
Before Width: | Height: | Size: 88 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 204 B |
Before Width: | Height: | Size: 233 B |
Before Width: | Height: | Size: 97 B |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 329 B |
Before Width: | Height: | Size: 271 B |
Before Width: | Height: | Size: 97 B |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 464 B |
Before Width: | Height: | Size: 329 B |
Before Width: | Height: | Size: 102 B |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 610 B |
Before Width: | Height: | Size: 380 B |
|
@ -1,87 +0,0 @@
|
|||
<?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>
|
|
@ -1,34 +0,0 @@
|
|||
<?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>
|
|
@ -1,17 +0,0 @@
|
|||
<?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 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
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}
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.widget._
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.EnsichatActivity
|
||||
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}
|
||||
|
||||
/**
|
||||
* Represents a single chat with another specific device.
|
||||
*/
|
||||
class ChatFragment extends ListFragment with OnClickListener {
|
||||
|
||||
/**
|
||||
* Fragments need to have a default constructor, so this is optional.
|
||||
*/
|
||||
def this(address: Address) {
|
||||
this
|
||||
this.address = address
|
||||
}
|
||||
|
||||
private lazy val activity = getActivity.asInstanceOf[EnsichatActivity]
|
||||
|
||||
private var address: Address = _
|
||||
|
||||
private var chatService: ConnectionHandler = _
|
||||
|
||||
private var sendButton: Button = _
|
||||
|
||||
private var messageText: EditText = _
|
||||
|
||||
private var listView: ListView = _
|
||||
|
||||
private var adapter: DatesAdapter = _
|
||||
|
||||
override def onActivityCreated(savedInstanceState: Bundle): Unit = {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
activity.runOnServiceConnected(() => {
|
||||
chatService = activity.service.get
|
||||
|
||||
activity.database.get.getContact(address).foreach(c => getActivity.setTitle(c.name))
|
||||
|
||||
adapter = new DatesAdapter(getActivity,
|
||||
new MessagesAdapter(getActivity, activity.database.get.getMessages(address), address))
|
||||
|
||||
if (listView != null) {
|
||||
listView.setAdapter(adapter)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
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]
|
||||
messageText.setOnEditorActionListener(new OnEditorActionListener {
|
||||
override def onEditorAction(view: TextView, actionId: Int, event: KeyEvent): Boolean = {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
onClick(sendButton)
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
})
|
||||
listView = view.findViewById(android.R.id.list).asInstanceOf[ListView]
|
||||
listView.setAdapter(adapter)
|
||||
view
|
||||
}
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null)
|
||||
address = new Address(savedInstanceState.getByteArray("address"))
|
||||
|
||||
LocalBroadcastManager.getInstance(getActivity)
|
||||
.registerReceiver(onMessageReceivedReceiver, new IntentFilter(CallbackHandler.ActionMessageReceived))
|
||||
}
|
||||
|
||||
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putByteArray("address", address.bytes)
|
||||
}
|
||||
|
||||
override def onDestroy(): Unit = {
|
||||
super.onDestroy()
|
||||
LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(onMessageReceivedReceiver)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message if send button was clicked.
|
||||
*/
|
||||
override def onClick(view: View): Unit = view.getId match {
|
||||
case R.id.send =>
|
||||
val text = messageText.getText.toString.trim
|
||||
if (!text.isEmpty) {
|
||||
val message = new Text(text.toString)
|
||||
chatService.sendTo(address, message)
|
||||
messageText.getText.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays new messages in UI.
|
||||
*/
|
||||
private val onMessageReceivedReceiver = new BroadcastReceiver {
|
||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||
val msg = intent.getSerializableExtra(CallbackHandler.ExtraMessage).asInstanceOf[Message]
|
||||
if (!Set(msg.header.origin, msg.header.target).contains(address))
|
||||
return
|
||||
|
||||
msg.body match {
|
||||
case _: Text =>
|
||||
val messages = activity.database.get.getMessages(address)
|
||||
adapter.replaceItems(messages)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
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 _ =>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
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]))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
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
|
||||
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
0
android/.gitignore → app/.gitignore
vendored
|
@ -1,17 +1,24 @@
|
|||
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:design:24.2.1'
|
||||
compile "com.android.support:appcompat-v7:23.0.0"
|
||||
compile 'com.android.support:multidex:1.0.1'
|
||||
compile 'org.scala-lang:scala-library:2.11.7'
|
||||
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 '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' }
|
||||
// TODO: use @aar maven dependency for bitcoin
|
||||
}
|
||||
|
||||
// RtlHardcoded behaviour differs between target API versions. We only care about API 15.
|
||||
|
@ -25,23 +32,21 @@ preBuild.doFirst {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion "24.0.2"
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "22.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.nutomic.ensichat"
|
||||
targetSdkVersion 24
|
||||
versionCode 17
|
||||
versionName "0.5.2"
|
||||
targetSdkVersion 23
|
||||
versionCode 7
|
||||
versionName "0.1.6"
|
||||
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.
|
|
@ -0,0 +1,39 @@
|
|||
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))
|
||||
}
|
||||
|
||||
}
|
|
@ -6,18 +6,15 @@ import android.test.AndroidTestCase
|
|||
|
||||
class BluetoothInterfaceTest extends AndroidTestCase {
|
||||
|
||||
private lazy val btInterface = new BluetoothInterface(getContext, new Handler(), null)
|
||||
private lazy val adapter = new BluetoothInterface(getContext, new Handler(), Message => Unit,
|
||||
() => Unit, Message => false)
|
||||
|
||||
/**
|
||||
* Test for issue [[https://github.com/Nutomic/ensichat/issues/3 #3]].
|
||||
*/
|
||||
def testStartBluetoothOff(): Unit = {
|
||||
val btAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
if (btAdapter == null)
|
||||
return
|
||||
|
||||
btAdapter.disable()
|
||||
btInterface.create()
|
||||
BluetoothAdapter.getDefaultAdapter.disable()
|
||||
adapter.create()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package com.nutomic.ensichat.core.routing
|
||||
package com.nutomic.ensichat.protocol
|
||||
|
||||
import com.nutomic.ensichat.core.routing.AddressTest._
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Assert._
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.AddressTest._
|
||||
import junit.framework.Assert._
|
||||
|
||||
object AddressTest {
|
||||
|
||||
|
@ -24,7 +24,7 @@ object AddressTest {
|
|||
|
||||
}
|
||||
|
||||
class AddressTest extends TestCase {
|
||||
class AddressTest extends AndroidTestCase {
|
||||
|
||||
def testEncode(): Unit = {
|
||||
Addresses.foreach{a =>
|
|
@ -0,0 +1,35 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
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 header =
|
||||
new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), false, i)
|
||||
val msg = new Message(header, 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), false)
|
||||
new Message(header, new UserInfo("", ""))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
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")
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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, body)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.nutomic.ensichat.protocol.body
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import junit.framework.Assert
|
||||
|
||||
|
||||
class PaymentInformationTest extends AndroidTestCase {
|
||||
|
||||
def testWriteRead(): Unit = {
|
||||
val pi = new PaymentInformation("testmessage".getBytes)
|
||||
val bytes = pi.write
|
||||
val body = PaymentInformation.read(bytes)
|
||||
Assert.assertEquals(pi, body)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
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])
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package com.nutomic.ensichat.protocol.header
|
||||
|
||||
import java.util.{Date, GregorianCalendar}
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.body.{PaymentInformation, 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), false, 5)
|
||||
|
||||
val h2 = new ContentHeader(AddressTest.a1, AddressTest.a3,
|
||||
30000, Text.Type, Some(8765), Some(new GregorianCalendar(2014, 6, 10).getTime), false, 20)
|
||||
|
||||
val h3 = new ContentHeader(AddressTest.a4, AddressTest.a2,
|
||||
250, Text.Type, Some(77), Some(new GregorianCalendar(2020, 11, 11).getTime), false, 123)
|
||||
|
||||
val h4 = new ContentHeader(Address.Null, Address.Broadcast,
|
||||
ContentHeader.SeqNumRange.last, 0, Some(0xffff), Some(new Date(0L)), false, 0xff)
|
||||
|
||||
val h5 = new ContentHeader(Address.Broadcast, Address.Null,
|
||||
0, 0xff, Some(0), Some(new Date(0xffffffffL)), false, 0)
|
||||
|
||||
val h6 = new ContentHeader(AddressTest.a1, AddressTest.a2, 1234,
|
||||
PaymentInformation.Type, Some(123), Some(new GregorianCalendar(2015, 8, 9).getTime), false, 5)
|
||||
|
||||
val headers = Set(h1, h2, h3, h4, h5, h6)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
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, PaymentInformation}
|
||||
import com.nutomic.ensichat.protocol.header.ContentHeader
|
||||
import com.nutomic.ensichat.protocol.header.ContentHeaderTest._
|
||||
import com.nutomic.ensichat.protocol.{AddressTest, Message, 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 testTextMessage(): Unit = {
|
||||
val msg = database.getMessages(m3.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(h3.contentType, header.contentType)
|
||||
assertEquals(h3.messageId, header.messageId)
|
||||
assertEquals(h3.time, header.time)
|
||||
assertEquals(h3.read, header.read)
|
||||
assertEquals(new CryptoData(None, None), msg.crypto)
|
||||
assertEquals(m3.body, msg.body)
|
||||
}
|
||||
|
||||
def testPaymentRequestMessage(): Unit = {
|
||||
val pr = new PaymentInformation("teststring".getBytes)
|
||||
val msg = new Message(h6, pr)
|
||||
database.onMessageReceived(msg)
|
||||
val retrieved = database.getMessages(h6.origin, 1).firstKey
|
||||
assertEquals(pr, retrieved.body)
|
||||
}
|
||||
|
||||
def testMessageRead(): Unit = {
|
||||
database.setMessageRead(h3)
|
||||
val header = database.getMessages(AddressTest.a4, 1).firstKey.header.asInstanceOf[ContentHeader]
|
||||
assertTrue(header.read)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -6,15 +6,11 @@
|
|||
|
||||
<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="false" />
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
|
@ -34,13 +30,11 @@
|
|||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.NoActionBar"/>
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.ConnectionsActivity"
|
||||
android:label="@string/connections"
|
||||
android:parentActivityName=".activities.MainActivity">
|
||||
android:name=".activities.AddContactsActivity"
|
||||
android:label="@string/add_contacts" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.MainActivity" />
|
||||
|
@ -48,20 +42,13 @@
|
|||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".activities.MainActivity">
|
||||
android:label="@string/settings" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".service.ChatService" />
|
||||
<service android:name=".protocol.ChatService" />
|
||||
|
||||
</application>
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
/**
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.schildbach.wallet.integration.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* @author Andreas Schildbach
|
||||
*/
|
||||
public final class BitcoinIntegration
|
||||
{
|
||||
private static final String INTENT_EXTRA_PAYMENTREQUEST = "paymentrequest";
|
||||
private static final String INTENT_EXTRA_PAYMENT = "payment";
|
||||
private static final String INTENT_EXTRA_TRANSACTION_HASH = "transaction_hash";
|
||||
|
||||
private static final String MIMETYPE_PAYMENTREQUEST = "application/bitcoin-paymentrequest"; // BIP 71
|
||||
|
||||
/**
|
||||
* Request any amount of Bitcoins (probably a donation) from user, without feedback from the app.
|
||||
*
|
||||
* @param context
|
||||
* Android context
|
||||
* @param address
|
||||
* Bitcoin address
|
||||
*/
|
||||
public static void request(final Context context, final String address)
|
||||
{
|
||||
final Intent intent = makeBitcoinUriIntent(address, null);
|
||||
|
||||
start(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request specific amount of Bitcoins from user, without feedback from the app.
|
||||
*
|
||||
* @param context
|
||||
* Android context
|
||||
* @param address
|
||||
* Bitcoin address
|
||||
* @param amount
|
||||
* Bitcoin amount in satoshis
|
||||
*/
|
||||
public static void request(final Context context, final String address, final long amount)
|
||||
{
|
||||
final Intent intent = makeBitcoinUriIntent(address, amount);
|
||||
|
||||
start(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payment from user, without feedback from the app.
|
||||
*
|
||||
* @param context
|
||||
* Android context
|
||||
* @param paymentRequest
|
||||
* BIP70 formatted payment request
|
||||
*/
|
||||
public static void request(final Context context, final byte[] paymentRequest)
|
||||
{
|
||||
final Intent intent = makePaymentRequestIntent(paymentRequest);
|
||||
|
||||
start(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request any amount of Bitcoins (probably a donation) from user, with feedback from the app. Result intent can be
|
||||
* received by overriding {@link android.app.Activity#onActivityResult()}. Result indicates either
|
||||
* {@link Activity#RESULT_OK} or {@link Activity#RESULT_CANCELED}. In the success case, use
|
||||
* {@link #transactionHashFromResult(Intent)} to read the transaction hash from the intent.
|
||||
*
|
||||
* Warning: A success indication is no guarantee! To be on the safe side, you must drive your own Bitcoin
|
||||
* infrastructure and validate the transaction.
|
||||
*
|
||||
* @param activity
|
||||
* Calling Android activity
|
||||
* @param requestCode
|
||||
* Code identifying the call when {@link android.app.Activity#onActivityResult()} is called back
|
||||
* @param address
|
||||
* Bitcoin address
|
||||
*/
|
||||
public static void requestForResult(final Activity activity, final int requestCode, final String address)
|
||||
{
|
||||
final Intent intent = makeBitcoinUriIntent(address, null);
|
||||
|
||||
startForResult(activity, requestCode, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request specific amount of Bitcoins from user, with feedback from the app. Result intent can be received by
|
||||
* overriding {@link android.app.Activity#onActivityResult()}. Result indicates either {@link Activity#RESULT_OK} or
|
||||
* {@link Activity#RESULT_CANCELED}. In the success case, use {@link #transactionHashFromResult(Intent)} to read the
|
||||
* transaction hash from the intent.
|
||||
*
|
||||
* Warning: A success indication is no guarantee! To be on the safe side, you must drive your own Bitcoin
|
||||
* infrastructure and validate the transaction.
|
||||
*
|
||||
* @param activity
|
||||
* Calling Android activity
|
||||
* @param requestCode
|
||||
* Code identifying the call when {@link android.app.Activity#onActivityResult()} is called back
|
||||
* @param address
|
||||
* Bitcoin address
|
||||
*/
|
||||
public static void requestForResult(final Activity activity, final int requestCode, final String address, final long amount)
|
||||
{
|
||||
final Intent intent = makeBitcoinUriIntent(address, amount);
|
||||
|
||||
startForResult(activity, requestCode, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request payment from user, with feedback from the app. Result intent can be received by overriding
|
||||
* {@link android.app.Activity#onActivityResult()}. Result indicates either {@link Activity#RESULT_OK} or
|
||||
* {@link Activity#RESULT_CANCELED}. In the success case, use {@link #transactionHashFromResult(Intent)} to read the
|
||||
* transaction hash from the intent.
|
||||
*
|
||||
* Warning: A success indication is no guarantee! To be on the safe side, you must drive your own Bitcoin
|
||||
* infrastructure and validate the transaction.
|
||||
*
|
||||
* @param activity
|
||||
* Calling Android activity
|
||||
* @param requestCode
|
||||
* Code identifying the call when {@link android.app.Activity#onActivityResult()} is called back
|
||||
* @param paymentRequest
|
||||
* BIP70 formatted payment request
|
||||
*/
|
||||
public static void requestForResult(final Activity activity, final int requestCode, final byte[] paymentRequest)
|
||||
{
|
||||
final Intent intent = makePaymentRequestIntent(paymentRequest);
|
||||
|
||||
startForResult(activity, requestCode, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment request from intent. Meant for usage by applications accepting payment requests.
|
||||
*
|
||||
* @param intent
|
||||
* intent
|
||||
* @return payment request or null
|
||||
*/
|
||||
public static byte[] paymentRequestFromIntent(final Intent intent)
|
||||
{
|
||||
final byte[] paymentRequest = intent.getByteArrayExtra(INTENT_EXTRA_PAYMENTREQUEST);
|
||||
|
||||
return paymentRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put BIP70 payment message into result intent. Meant for usage by Bitcoin wallet applications.
|
||||
*
|
||||
* @param result
|
||||
* result intent
|
||||
* @param payment
|
||||
* payment message
|
||||
*/
|
||||
public static void paymentToResult(final Intent result, final byte[] payment)
|
||||
{
|
||||
result.putExtra(INTENT_EXTRA_PAYMENT, payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get BIP70 payment message from result intent. Meant for usage by applications initiating a Bitcoin payment.
|
||||
*
|
||||
* You can use the transactions contained in the payment to validate the payment. For this, you need your own
|
||||
* Bitcoin infrastructure though. There is no guarantee that the payment will ever confirm.
|
||||
*
|
||||
* @param result
|
||||
* result intent
|
||||
* @return payment message
|
||||
*/
|
||||
public static byte[] paymentFromResult(final Intent result)
|
||||
{
|
||||
final byte[] payment = result.getByteArrayExtra(INTENT_EXTRA_PAYMENT);
|
||||
|
||||
return payment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put transaction hash into result intent. Meant for usage by Bitcoin wallet applications.
|
||||
*
|
||||
* @param result
|
||||
* result intent
|
||||
* @param txHash
|
||||
* transaction hash
|
||||
*/
|
||||
public static void transactionHashToResult(final Intent result, final String txHash)
|
||||
{
|
||||
result.putExtra(INTENT_EXTRA_TRANSACTION_HASH, txHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction hash from result intent. Meant for usage by applications initiating a Bitcoin payment.
|
||||
*
|
||||
* You can use this hash to request the transaction from the Bitcoin network, in order to validate. For this, you
|
||||
* need your own Bitcoin infrastructure though. There is no guarantee that the transaction has ever been broadcasted
|
||||
* to the Bitcoin network.
|
||||
*
|
||||
* @param result
|
||||
* result intent
|
||||
* @return transaction hash
|
||||
*/
|
||||
public static String transactionHashFromResult(final Intent result)
|
||||
{
|
||||
final String txHash = result.getStringExtra(INTENT_EXTRA_TRANSACTION_HASH);
|
||||
|
||||
return txHash;
|
||||
}
|
||||
|
||||
private static final int SATOSHIS_PER_COIN = 100000000;
|
||||
|
||||
private static Intent makeBitcoinUriIntent(final String address, final Long amount)
|
||||
{
|
||||
final StringBuilder uri = new StringBuilder("bitcoin:");
|
||||
if (address != null)
|
||||
uri.append(address);
|
||||
if (amount != null)
|
||||
uri.append("?amount=").append(String.format("%d.%08d", amount / SATOSHIS_PER_COIN, amount % SATOSHIS_PER_COIN));
|
||||
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri.toString()));
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static Intent makePaymentRequestIntent(final byte[] paymentRequest)
|
||||
{
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setType(MIMETYPE_PAYMENTREQUEST);
|
||||
intent.putExtra(INTENT_EXTRA_PAYMENTREQUEST, paymentRequest);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static void start(final Context context, final Intent intent)
|
||||
{
|
||||
final PackageManager pm = context.getPackageManager();
|
||||
if (pm.resolveActivity(intent, 0) != null)
|
||||
context.startActivity(intent);
|
||||
else
|
||||
redirectToDownload(context);
|
||||
}
|
||||
|
||||
private static void startForResult(final Activity activity, final int requestCode, final Intent intent)
|
||||
{
|
||||
final PackageManager pm = activity.getPackageManager();
|
||||
if (pm.resolveActivity(intent, 0) != null)
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
else
|
||||
redirectToDownload(activity);
|
||||
}
|
||||
|
||||
private static void redirectToDownload(final Context context)
|
||||
{
|
||||
Toast.makeText(context, "No Bitcoin application found.\nPlease install Bitcoin Wallet.", Toast.LENGTH_LONG).show();
|
||||
|
||||
final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=de.schildbach.wallet"));
|
||||
final Intent binaryIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/schildbach/bitcoin-wallet/releases"));
|
||||
|
||||
final PackageManager pm = context.getPackageManager();
|
||||
if (pm.resolveActivity(marketIntent, 0) != null)
|
||||
context.startActivity(marketIntent);
|
||||
else if (pm.resolveActivity(binaryIntent, 0) != null)
|
||||
context.startActivity(binaryIntent);
|
||||
// else out of luck
|
||||
}
|
||||
}
|
BIN
app/src/main/res/drawable-hdpi/ic_action_add_person.png
Normal file
After Width: | Height: | Size: 616 B |
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 465 B |
BIN
app/src/main/res/drawable-hdpi/ic_bitcoin.png
Normal file
After Width: | Height: | Size: 957 B |
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_action_add_person.png
Normal file
After Width: | Height: | Size: 469 B |
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
BIN
app/src/main/res/drawable-mdpi/ic_bitcoin.png
Normal file
After Width: | Height: | Size: 635 B |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_action_add_person.png
Normal file
After Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
BIN
app/src/main/res/drawable-xhdpi/ic_bitcoin.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_add_person.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 759 B After Width: | Height: | Size: 759 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_bitcoin.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -26,7 +26,7 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_connections"
|
||||
android:text="@string/devices_empty"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,6 +1,5 @@
|
|||
<?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,28 +1,12 @@
|
|||
<?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"
|
||||
|
@ -52,13 +36,18 @@
|
|||
android:id="@+id/linearLayout"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:id="@+id/send_bitcoin"
|
||||
android:src="@drawable/ic_bitcoin"
|
||||
android:background="@android:color/transparent"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/message"
|
||||
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"/>
|
||||
|
20
app/src/main/res/layout/fragment_contacts.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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>
|
20
app/src/main/res/layout/fragment_identicon.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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,6 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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" />
|
||||
|
||||
<item
|
||||
android:id="@+id/share_app"
|
||||
|
@ -14,4 +21,8 @@
|
|||
android:id="@+id/settings"
|
||||
android:title="@string/settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/exit"
|
||||
android:title="@string/exit" />
|
||||
|
||||
</menu>
|
|
@ -14,8 +14,4 @@
|
|||
<!-- 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>
|
19
app/src/main/res/values/settings_defaults.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources translatable="false">
|
||||
|
||||
<string name="default_user_status">Let\'s chat!</string>
|
||||
|
||||
<bool name="default_notification_sounds">true</bool>
|
||||
|
||||
<string name="default_scan_interval">15</string>
|
||||
|
||||
<string name="default_max_connections">1000000</string>
|
||||
|
||||
<string-array name="bitcoin_wallets">
|
||||
<item>de.schildbach.wallet</item>
|
||||
<item>de.schildbach.wallet_test</item>
|
||||
</string-array>
|
||||
|
||||
<string name="default_bitcoin_wallet">de.schildbach.wallet</string>
|
||||
|
||||
</resources>
|
|
@ -18,42 +18,32 @@
|
|||
<!-- 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="toast_bluetooth_denied">Please enable Bluetooth to connect with devices near you</string>
|
||||
<string name="bluetooth_required">Bluetooth is required for this app.</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>
|
||||
|
||||
<!-- ChatFragment -->
|
||||
|
||||
<!-- Hint text for the message EditText -->
|
||||
<string name="message_hint">Type a message</string>
|
||||
<!-- Menu item to close app and stop service -->
|
||||
<string name="exit">Exit</string>
|
||||
|
||||
|
||||
<!-- ConnectionsActivity -->
|
||||
<!-- AddContactsActivity -->
|
||||
|
||||
<!-- Activity title -->
|
||||
<string name="connections">Connections</string>
|
||||
<string name="add_contacts">Add Contacts</string>
|
||||
|
||||
<!-- Empty text for devices list -->
|
||||
<string name="no_connections">Searching for Users\nRange: ~10m</string>
|
||||
<string name="devices_empty">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>
|
||||
|
@ -61,16 +51,10 @@
|
|||
<!-- 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>
|
||||
<!-- ChatFragment -->
|
||||
|
||||
<string name="invalid_address">Invalid address</string>
|
||||
|
||||
<string name="add_contact">Add Contact</string>
|
||||
|
||||
<string name="scan_qr_code">Scan QR-Code</string>
|
||||
<string name="bitcoin_wallet_not_found">Bitcoin wallet not found. Please install wallet app or select another wallet in the app settings.</string>
|
||||
|
||||
|
||||
<!-- SettingsActivity -->
|
||||
|
@ -93,8 +77,16 @@
|
|||
<!-- Preference title -->
|
||||
<string name="notification_sounds">Notification Sounds</string>
|
||||
|
||||
<!-- Preference title -->
|
||||
<string name="servers">Servers</string>
|
||||
<string name="bitcoin_wallet">Bitcoin Wallet Application</string>
|
||||
|
||||
<!-- List of supported wallet apps -->
|
||||
<string-array name="bitcoin_wallet_values">
|
||||
<item>Bitcoin Wallet</item>
|
||||
<item>Bitcoin Wallet for Testnet</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Preference title (debug only)-->
|
||||
<string name="max_connections" translatable="false">Maximum Number of Connections</string>
|
||||
|
||||
<!-- Preference title -->
|
||||
<string name="report_issue">Report Issue</string>
|
||||
|
@ -107,26 +99,15 @@
|
|||
<!-- Preference title -->
|
||||
<string name="version">Version</string>
|
||||
|
||||
|
||||
<!-- UserInfoFragment -->
|
||||
<!-- IdenticonFragment -->
|
||||
|
||||
<!-- 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,11 +9,6 @@
|
|||
<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,27 +4,37 @@
|
|||
|
||||
<EditTextPreference
|
||||
android:title="@string/user_name"
|
||||
android:key="user_name"
|
||||
android:inputType="textCapWords"/>
|
||||
android:key="user_name" />
|
||||
|
||||
<EditTextPreference
|
||||
android:title="@string/user_status"
|
||||
android:key="user_status"
|
||||
android:inputType="textCapSentences" />
|
||||
android:key="user_status" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:title="@string/notification_sounds"
|
||||
android:key="notification_sounds" />
|
||||
android:key="notification_sounds"
|
||||
android:defaultValue="@bool/default_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" />
|
||||
|
||||
<ListPreference
|
||||
android:title="@string/bitcoin_wallet"
|
||||
android:key="bitcoin_wallet"
|
||||
android:entries="@array/bitcoin_wallet_values"
|
||||
android:entryValues="@array/bitcoin_wallets"
|
||||
android:defaultValue="@string/default_bitcoin_wallet" />
|
||||
|
||||
<EditTextPreference
|
||||
android:title="@string/servers"
|
||||
android:key="servers" />
|
||||
android:title="@string/max_connections"
|
||||
android:key="max_connections"
|
||||
android:defaultValue="@string/default_max_connections"
|
||||
android:inputType="number"
|
||||
android:numeric="integer" />
|
||||
|
||||
<Preference
|
||||
android:title="@string/report_issue"
|
||||
|
@ -36,6 +46,7 @@
|
|||
|
||||
<Preference
|
||||
android:title="@string/version"
|
||||
android:key="version" />
|
||||
android:key="version"
|
||||
style="?android:preferenceInformationStyle" />
|
||||
|
||||
</PreferenceScreen>
|
|
@ -0,0 +1,94 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.service.ChatService
|
||||
import com.nutomic.ensichat.protocol.{ChatService, ChatServiceBinder}
|
||||
|
||||
/**
|
||||
* 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[ChatService.Binder]
|
||||
val binder = iBinder.asInstanceOf[ChatServiceBinder]
|
||||
chatService = Option(binder.service)
|
||||
listeners.foreach(_())
|
||||
listeners = Set.empty
|
||||
|
@ -58,10 +58,8 @@ class EnsichatActivity extends AppCompatActivity with ServiceConnection {
|
|||
/**
|
||||
* Returns the [[ChatService]].
|
||||
*
|
||||
* Will only be set after [[runOnServiceConnected]].
|
||||
* Should only be called after [[runOnServiceConnected]] callback was called.
|
||||
*/
|
||||
def service = chatService.map(_.getConnectionHandler)
|
||||
|
||||
def database = chatService.map(_.database)
|
||||
def service = chatService.get
|
||||
|
||||
}
|
|
@ -1,22 +1,17 @@
|
|||
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, Toast}
|
||||
import android.widget.{Button, EditText, TextView}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
||||
import com.nutomic.ensichat.fragments.SettingsFragment
|
||||
|
||||
/**
|
||||
* Shown on first start, lets the user enter their name.
|
||||
|
@ -24,7 +19,6 @@ import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
|||
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)
|
||||
|
@ -36,24 +30,19 @@ 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)
|
||||
|
||||
val name = Option(BluetoothAdapter.getDefaultAdapter).map(_.getName.trim).getOrElse("")
|
||||
username.setText(name)
|
||||
username.setText(BluetoothAdapter.getDefaultAdapter.getName.trim)
|
||||
username.setOnEditorActionListener(this)
|
||||
done.setOnClickListener(this)
|
||||
|
||||
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()
|
||||
}
|
||||
imm.showSoftInput(username, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,7 +60,7 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
|
|||
override def onClick(v: View): Unit = save()
|
||||
|
||||
/**
|
||||
* Saves username and default settings values, then calls [[startMainActivity]].
|
||||
* Saves values and calls [[startMainActivity]].
|
||||
*/
|
||||
private def save(): Unit = {
|
||||
imm.hideSoftInputFromWindow(username.getWindowToken, 0)
|
||||
|
@ -79,27 +68,11 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
|
|||
preferences
|
||||
.edit()
|
||||
.putBoolean(KeyIsFirstStart, false)
|
||||
.putString(KeyUserName, username.getText.toString.trim)
|
||||
.putString(KeyUserStatus, SettingsInterface.DefaultUserStatus)
|
||||
.putBoolean(KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
|
||||
.putString(KeyScanInterval, DefaultScanInterval.toString)
|
||||
.putString(KeyAddresses, DefaultAddresses)
|
||||
.putString(SettingsFragment.KeyUserName, username.getText.toString.trim)
|
||||
.putString(SettingsFragment.KeyUserStatus, getString(R.string.default_user_status))
|
||||
.apply()
|
||||
|
||||
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()
|
||||
}
|
||||
startMainActivity()
|
||||
}
|
||||
|
||||
def startMainActivity(): Unit = {
|
|
@ -4,12 +4,11 @@ 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 {
|
||||
|
||||
|
@ -25,8 +24,6 @@ object MainActivity {
|
|||
|
||||
val ExtraAddress = "address"
|
||||
|
||||
val PrefWasBluetoothEnabled = "was_bluetooth_enabled"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,14 +44,7 @@ class MainActivity extends EnsichatActivity {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
if (getIntent.getAction == MainActivity.ActionRequestBluetooth &&
|
||||
Option(BluetoothAdapter.getDefaultAdapter).isDefined) {
|
||||
val btAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putBoolean(MainActivity.PrefWasBluetoothEnabled, btAdapter.isEnabled)
|
||||
.apply()
|
||||
|
||||
if (getIntent.getAction == MainActivity.ActionRequestBluetooth) {
|
||||
val intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
|
||||
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
|
||||
startActivityForResult(intent, RequestSetDiscoverable)
|
||||
|
@ -99,7 +89,8 @@ class MainActivity extends EnsichatActivity {
|
|||
requestCode match {
|
||||
case RequestSetDiscoverable =>
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(this, R.string.toast_bluetooth_denied, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this, R.string.bluetooth_required, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,7 +104,7 @@ class MainActivity extends EnsichatActivity {
|
|||
.detach(contactsFragment)
|
||||
.add(android.R.id.content, new ChatFragment(address))
|
||||
.commit()
|
||||
Option(getSupportActionBar).foreach(_.setDisplayHomeAsUpEnabled(true))
|
||||
getSupportActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -135,8 +126,7 @@ class MainActivity extends EnsichatActivity {
|
|||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
|
||||
case android.R.id.home =>
|
||||
if (currentChat.isDefined)
|
||||
onBackPressed()
|
||||
onBackPressed()
|
||||
true;
|
||||
case _ =>
|
||||
super.onOptionsItemSelected(item);
|
|
@ -7,14 +7,12 @@ 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.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 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 scala.collection.immutable.HashMap
|
||||
|
||||
|
@ -31,25 +29,27 @@ object BluetoothInterface {
|
|||
* Handles all Bluetooth connectivity.
|
||||
*/
|
||||
class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||
connectionHandler: ConnectionHandler) extends TransmissionInterface {
|
||||
onMessageReceived: Message => Unit, callConnectionListeners: () => Unit,
|
||||
onConnectionOpened: (Message) => Boolean)
|
||||
extends InterfaceHandler {
|
||||
|
||||
private val Tag = "BluetoothInterface"
|
||||
|
||||
private lazy val btAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
|
||||
private lazy val crypto = ChatService.newCrypto(context)
|
||||
private lazy val crypto = new Crypto(context)
|
||||
|
||||
private var devices = new HashMap[Device.ID, Device]()
|
||||
|
||||
private var connections = new HashMap[Device.ID, BluetoothTransferThread]()
|
||||
private var connections = new HashMap[Device.ID, TransferThread]()
|
||||
|
||||
private var listenThread: Option[BluetoothListenThread] = None
|
||||
private var listenThread: Option[ListenThread] = None
|
||||
|
||||
private var cancelDiscovery = false
|
||||
|
||||
private var discovered = Set[Device]()
|
||||
|
||||
private var addressDeviceMap = new HashMap[Address, Device.ID]()
|
||||
private val addressDeviceMap = HashBiMap.create[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.foreach(_.cancel())
|
||||
listenThread.get.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 BluetoothListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
|
||||
listenThread = Some(new ListenThread(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(SettingsInterface.KeyScanInterval, SettingsInterface.DefaultScanInterval.toString).toInt * 1000
|
||||
val scanInterval = pm.getString(SettingsFragment.KeyScanInterval,
|
||||
context.getResources.getString(R.string.default_scan_interval)).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 BluetoothConnectThread(d, connectionOpened).start()
|
||||
new ConnectThread(d, connectionOpened).start()
|
||||
devices += (d.id -> d)
|
||||
}
|
||||
discovered = Set[Device]()
|
||||
|
@ -165,20 +165,18 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
|||
def connectionOpened(device: Device, socket: BluetoothSocket): Unit = {
|
||||
devices += (device.id -> device)
|
||||
connections += (device.id ->
|
||||
new BluetoothTransferThread(context, device, socket, this, crypto, onReceiveMessage))
|
||||
new TransferThread(context, device, socket, this, crypto, onReceiveMessage))
|
||||
connections(device.id).start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes device from active connections.
|
||||
*/
|
||||
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)
|
||||
def onConnectionClosed(device: Device, socket: BluetoothSocket): Unit = {
|
||||
devices -= device.id
|
||||
connections -= device.id
|
||||
callConnectionListeners()
|
||||
addressDeviceMap.inverse().remove(device.id)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -193,31 +191,23 @@ 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 += (address -> device)
|
||||
if (!connectionHandler.onConnectionOpened(msg))
|
||||
addressDeviceMap -= address
|
||||
addressDeviceMap.put(address, device)
|
||||
if (!onConnectionOpened(msg))
|
||||
addressDeviceMap.remove(address)
|
||||
case _ =>
|
||||
connectionHandler.onMessageReceived(msg, getAddressForDevice(device))
|
||||
onMessageReceived(msg)
|
||||
}
|
||||
|
||||
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 = {
|
||||
addressDeviceMap
|
||||
.find(_._1 == nextHop || Address.Broadcast == nextHop)
|
||||
.map(i => connections.get(i._2))
|
||||
.getOrElse(None)
|
||||
.foreach(_.send(msg))
|
||||
}
|
||||
override def send(nextHop: Address, msg: Message): Unit =
|
||||
connections.get(addressDeviceMap.get(nextHop)).foreach(_.send(msg))
|
||||
|
||||
/**
|
||||
* Returns all active Bluetooth connections.
|
||||
*/
|
||||
override def getConnections: Set[Address] =
|
||||
connections.map( c => getAddressForDevice(c._1)).toSet
|
||||
def getConnections: Set[Address] =
|
||||
connections.map(x => addressDeviceMap.inverse().get(x._1)).toSet
|
||||
|
||||
}
|
|
@ -8,30 +8,22 @@ import android.util.Log
|
|||
/**
|
||||
* Attempts to connect to another device and calls [[onConnected]] on success.
|
||||
*/
|
||||
class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
||||
class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
||||
|
||||
private val Tag = "ConnectThread"
|
||||
|
||||
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
|
||||
}
|
||||
private val socket =
|
||||
device.btDevice.get.createInsecureRfcommSocketToServiceRecord(BluetoothInterface.AppUuid)
|
||||
|
||||
override def run(): Unit = {
|
||||
if (socket.isEmpty)
|
||||
return
|
||||
|
||||
Log.i(Tag, "Connecting to " + device.toString)
|
||||
try {
|
||||
socket.get.connect()
|
||||
socket.connect()
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
Log.v(Tag, "Failed to connect to " + device.toString, e)
|
||||
try {
|
||||
socket.get.close()
|
||||
socket.close()
|
||||
} catch {
|
||||
case e2: IOException =>
|
||||
Log.e(Tag, "Failed to close socket", e2)
|
||||
|
@ -40,7 +32,7 @@ class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSock
|
|||
}
|
||||
|
||||
Log.i(Tag, "Successfully connected to device " + device.name)
|
||||
onConnected(new Device(device.btDevice.get, true), socket.get)
|
||||
onConnected(new Device(device.btDevice.get, true), socket)
|
||||
}
|
||||
|
||||
}
|