Compare commits

..

1 commit

Author SHA1 Message Date
c350ac12e0 Allow sending bitcoins in chat. 2015-09-12 20:23:28 +02:00
194 changed files with 3319 additions and 5448 deletions

3
.gitignore vendored
View file

@ -4,5 +4,4 @@
/build
*.iml
*.iws
*.ipr
*.apk
*.ipr

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "buildSrc"]
path = buildSrc
url = https://github.com/xelnaga/gradle-android-scala-plugin.git

View file

@ -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

1046
LICENSE

File diff suppressed because it is too large Load diff

View file

@ -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.

View file

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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 _ =>
}
}
}
}

View file

@ -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)
}
}

View file

@ -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 _ =>
}
}
}

View file

@ -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
}
}

View file

@ -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]))
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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.

View file

@ -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))
}
}

View file

@ -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()
}
}

View file

@ -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 =>

View file

@ -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)
}
}
}

View file

@ -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))
}
}
}

View file

@ -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("", ""))
}
}

View file

@ -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")
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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])
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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>

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

View file

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

View file

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

View file

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 759 B

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -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>

View file

@ -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"

View file

@ -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"/>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)
}
})
}
}
}

View file

@ -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
}

View file

@ -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 = {

View file

@ -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);

View file

@ -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
}

View file

@ -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)
}
}

Some files were not shown because too many files have changed in this diff Show more