diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7f6f5fa..d032373 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,6 +34,11 @@
android:value=".activities.MainActivity" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cd0ee1f..03f5562 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,6 +27,12 @@
Add Contacts
+
+ No nearby devices found
+
+
+
+
Do you want to add %1$s as a new contact?
@@ -39,15 +45,12 @@
Before accepting, make sure the images match on both devices
-
- No nearby devices found
+
+ Contact not added (denied by other device)
%1$s was added as a contact
-
- Contact not added (denied by other device)
-
@@ -60,4 +63,10 @@
Scan Interval (seconds)
+
+
+
+
+ New friend request!
+
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
index f125063..102a1bb 100644
--- a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
@@ -1,51 +1,26 @@
package com.nutomic.ensichat.activities
-import android.app.AlertDialog
-import android.content.DialogInterface.OnClickListener
-import android.content.{Intent, Context, DialogInterface}
+import android.content.Intent
import android.os.Bundle
import android.support.v4.app.NavUtils
-import android.util.Log
import android.view._
import android.widget.AdapterView.OnItemClickListener
import android.widget._
import com.nutomic.ensichat.R
-import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
-import com.nutomic.ensichat.protocol.{User, Address, ChatService, Crypto}
-import com.nutomic.ensichat.util.{Database, UsersAdapter, IdenticonGenerator}
-
-import scala.collection.SortedSet
+import com.nutomic.ensichat.protocol.ChatService
+import com.nutomic.ensichat.protocol.messages.RequestAddContact
+import com.nutomic.ensichat.util.UsersAdapter
/**
- * Lists all nearby, connected devices and allows adding them to contacts.
- *
- * Adding a contact requires confirmation on both sides.
+ * Lists all nearby, connected devices and allows adding them to be added as contacts.
*/
class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnectionsChangedListener
- with OnItemClickListener with ChatService.OnMessageReceivedListener {
+ with OnItemClickListener {
private val Tag = "AddContactsActivity"
private lazy val Adapter = new UsersAdapter(this)
- private lazy val Database = service.Database
-
- private lazy val Crypto = new Crypto(this)
-
- /**
- * Map of devices that should be added.
- */
- private var currentlyAdding = Map[User, AddContactInfo]()
- .withDefaultValue(new AddContactInfo(false, false))
-
- /**
- * Holds confirmation status for adding contacts.
- *
- * @param localConfirmed If true, the local user has accepted adding the contact.
- * @param remoteConfirmed If true, the remote contact has accepted adding this device as contact.
- */
- private case class AddContactInfo(localConfirmed: Boolean, remoteConfirmed: Boolean)
-
/**
* Initializes layout, registers connection and message listeners.
*/
@@ -61,8 +36,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
runOnServiceConnected(() => {
service.registerConnectionListener(AddContactsActivity.this)
- service.registerMessageListener(this)
- Database.runOnContactsUpdated(updateList)
+ service.Database.runOnContactsUpdated(updateList)
})
}
@@ -74,95 +48,9 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
val contact = Adapter.getItem(position)
service.sendTo(contact.Address, new RequestAddContact())
- addDeviceDialog(contact)
- }
-
- /**
- * Shows a dialog to accept/deny adding a device as a new contact.
- */
- private def addDeviceDialog(contact: User): Unit = {
- // Listener for dialog button clicks.
- val onClick = new OnClickListener {
- override def onClick(dialogInterface: DialogInterface, i: Int): Unit = i match {
- case DialogInterface.BUTTON_POSITIVE =>
- // Local user accepted contact, update state and send info to other device.
- currentlyAdding +=
- (contact -> new AddContactInfo(currentlyAdding(contact).localConfirmed, true))
- addContactIfBothConfirmed(contact)
- service.sendTo(contact.Address, new ResultAddContact(true))
- case DialogInterface.BUTTON_NEGATIVE =>
- // Local user denied adding contact, send info to other device.
- service.sendTo(contact.Address, new ResultAddContact(false))
- }
- }
-
- val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
- val view = inflater.inflate(R.layout.dialog_add_contact, null)
-
- val local = view.findViewById(R.id.local_identicon).asInstanceOf[ImageView]
- local.setImageBitmap(
- IdenticonGenerator.generate(Crypto.getLocalAddress, (150, 150), this))
- val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
- remoteTitle.setText(getString(R.string.remote_fingerprint_title, contact.Name))
- val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView]
- remote.setImageBitmap(IdenticonGenerator.generate(contact.Address, (150, 150), this))
-
- new AlertDialog.Builder(this)
- .setTitle(getString(R.string.add_contact_dialog, contact.Name))
- .setView(view)
- .setPositiveButton(android.R.string.yes, onClick)
- .setNegativeButton(android.R.string.no, onClick)
- .show()
- }
-
- /**
- * Handles incoming [[RequestAddContact]] and [[ResultAddContact]] messages.
- *
- * These are only handled here and require user action, so contacts can only be added if
- * the user is in this activity.
- */
- override def onMessageReceived(msg: Message): Unit = {
- if (msg.Header.Target != Crypto.getLocalAddress)
- return
-
- msg.Body match {
- case _: RequestAddContact =>
- Log.i(Tag, "Remote device " + msg.Header.Origin + " wants to add us as a contact, showing dialog")
- service.getConnections.find(_.Address == msg.Header.Origin).foreach(addDeviceDialog)
- case m: ResultAddContact =>
- currentlyAdding.keys.find(_.Address == msg.Header.Origin).foreach(contact =>
- if (m.Accepted) {
- Log.i(Tag, contact.toString + " accepted us as a contact, updating state")
- currentlyAdding += (contact ->
- new AddContactInfo(true, currentlyAdding(contact).remoteConfirmed))
- addContactIfBothConfirmed(contact)
- val intent = new Intent(this, classOf[MainActivity])
- intent.setAction(MainActivity.ActionOpenChat)
- intent.putExtra(MainActivity.ExtraAddress, msg.Header.Origin.toString)
- startActivity(intent)
- finish()
- } else {
- Log.i(Tag, contact.toString + " denied us as a contact, showing toast")
- Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
- currentlyAdding -= contact
- })
- case _ =>
- }
- }
-
- /**
- * Add the given device to contacts if [[AddContactInfo.localConfirmed]] and
- * [[AddContactInfo.remoteConfirmed]] are true for it in [[currentlyAdding]].
- */
- private def addContactIfBothConfirmed(contact: User): Unit = {
- val info = currentlyAdding(contact)
- if (info.localConfirmed && info.remoteConfirmed) {
- Log.i(Tag, "Adding new contact " + contact.toString)
- Database.addContact(contact)
- Toast.makeText(this, getString(R.string.contact_added, contact.Name), Toast.LENGTH_SHORT)
- .show()
- currentlyAdding -= contact
- }
+ val intent = new Intent(this, classOf[ConfirmAddContactDialog])
+ intent.putExtra(ConfirmAddContactDialog.ExtraContactAddress, contact.Address.toString)
+ startActivity(intent)
}
override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {
diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactDialog.scala b/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactDialog.scala
new file mode 100644
index 0000000..3810bcd
--- /dev/null
+++ b/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactDialog.scala
@@ -0,0 +1,121 @@
+package com.nutomic.ensichat.activities
+
+import android.app.AlertDialog
+import android.content.DialogInterface.OnClickListener
+import android.content.{Context, DialogInterface}
+import android.os.Bundle
+import android.util.Log
+import android.view.{ContextThemeWrapper, LayoutInflater}
+import android.widget.{ImageView, TextView, Toast}
+import com.nutomic.ensichat.R
+import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
+import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
+import com.nutomic.ensichat.protocol.{Address, Crypto}
+import com.nutomic.ensichat.util.IdenticonGenerator
+
+object ConfirmAddContactDialog {
+
+ val ExtraContactAddress = "contact_address"
+
+}
+
+/**
+ * Shows a dialog for adding a new contact (including key fingerprints).
+ */
+class ConfirmAddContactDialog extends EnsiChatActivity with OnMessageReceivedListener
+ with OnClickListener {
+
+ private val Tag = "ConfirmAddContactDialog"
+
+ private lazy val User = service.getUser(
+ new Address(getIntent.getStringExtra(ConfirmAddContactDialog.ExtraContactAddress)))
+
+ private var localConfirmed = false
+
+ private var remoteConfirmed = false
+
+ override def onCreate(savedInstanceState: Bundle): Unit = {
+ super.onCreate(savedInstanceState)
+
+ runOnServiceConnected(() => {
+ showDialog()
+ service.registerMessageListener(this)
+ })
+ }
+
+ /**
+ * Shows a dialog to accept/deny adding a device as a new contact.
+ */
+ private def showDialog(): Unit = {
+ val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
+ val view = inflater.inflate(R.layout.dialog_add_contact, null)
+
+ val local = view.findViewById(R.id.local_identicon).asInstanceOf[ImageView]
+ val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView]
+ val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
+
+ local.setImageBitmap(IdenticonGenerator.generate(Crypto.getLocalAddress(this), (150, 150), this))
+ remote.setImageBitmap(IdenticonGenerator.generate(User.Address, (150, 150), this))
+ remoteTitle.setText(getString(R.string.remote_fingerprint_title, User.Name))
+
+ new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme))
+ .setTitle(getString(R.string.add_contact_dialog, User.Name))
+ .setView(view)
+ .setCancelable(false)
+ .setPositiveButton(android.R.string.yes, this)
+ .setNegativeButton(android.R.string.no, this)
+ .show()
+ }
+
+ override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
+ val result = i match {
+ case DialogInterface.BUTTON_POSITIVE =>
+ localConfirmed = true
+ addContactIfBothConfirmed()
+ true
+ case DialogInterface.BUTTON_NEGATIVE =>
+ false
+ }
+ service.sendTo(User.Address, new ResultAddContact(result))
+ }
+
+ /**
+ * Add the user to contacts if [[localConfirmed]] and [[remoteConfirmed]] are true.
+ */
+ private def addContactIfBothConfirmed(): Unit = {
+ if (localConfirmed && remoteConfirmed) {
+ Log.i(Tag, "Adding new contact " + User.toString)
+ // Get the user again, in case
+ service.Database.addContact(service.getUser(User.Address))
+ Toast.makeText(this, getString(R.string.contact_added, User.Name), Toast.LENGTH_SHORT)
+ .show()
+ finish()
+ }
+ }
+
+ /**
+ * Handles incoming [[RequestAddContact]] and [[ResultAddContact]] messages.
+ *
+ * These are only handled here and require user action, so contacts can only be added if
+ * the user is in this activity.
+ */
+ override def onMessageReceived(msg: Message): Unit = {
+ if (msg.Header.Origin != User.Address || msg.Header.Target != Crypto.getLocalAddress(this))
+ return
+
+ msg.Body match {
+ case m: ResultAddContact =>
+ if (m.Accepted) {
+ Log.i(Tag, User.toString + " accepted us as a contact, updating state")
+ remoteConfirmed = true
+ addContactIfBothConfirmed()
+ } else {
+ Log.i(Tag, User.toString + " denied us as a contact, showing toast")
+ Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
+ finish()
+ }
+ case _ =>
+ }
+ }
+
+}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
index 1ed8812..e65d37f 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala
@@ -1,19 +1,19 @@
package com.nutomic.ensichat.protocol
-import android.app.Service
+import android.app.{Notification, NotificationManager, PendingIntent, Service}
import android.bluetooth.BluetoothAdapter
-import android.content.Intent
+import android.content.{Context, Intent}
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
+import com.nutomic.ensichat.R
+import com.nutomic.ensichat.activities.ConfirmAddContactDialog
import com.nutomic.ensichat.bluetooth.BluetoothInterface
import com.nutomic.ensichat.fragments.SettingsFragment
-import com.nutomic.ensichat.protocol.ChatService.{OnMessageReceivedListener, OnConnectionsChangedListener}
+import com.nutomic.ensichat.protocol.ChatService.{OnConnectionsChangedListener, OnMessageReceivedListener}
import com.nutomic.ensichat.protocol.messages._
import com.nutomic.ensichat.util.Database
-import scala.collection.SortedSet
-import scala.collection.immutable.HashMap
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.ref.WeakReference
@@ -57,9 +57,11 @@ class ChatService extends Service {
private lazy val Binder = new ChatServiceBinder(this)
- private lazy val Crypto = new Crypto(this)
+ private lazy val crypto = new Crypto(this)
- private lazy val BluetoothInterface = new BluetoothInterface(this, Crypto)
+ private lazy val bluetoothInterface = new BluetoothInterface(this, crypto)
+
+ private val notificationIdGenerator = Stream.from(100)
/**
* For this (and [[messageListeners]], functions would be useful instead of instances,
@@ -91,15 +93,15 @@ class ChatService extends Service {
registerMessageListener(Database)
Future {
- Crypto.generateLocalKeys()
+ crypto.generateLocalKeys()
- BluetoothInterface.create()
- Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
+ bluetoothInterface.create()
+ Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress(this))
}
}
override def onDestroy(): Unit = {
- BluetoothInterface.destroy()
+ bluetoothInterface.destroy()
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
@@ -125,15 +127,15 @@ class ChatService extends Service {
* Sends a new message to the given target address.
*/
def sendTo(target: Address, body: MessageBody): Unit = {
- if (!BluetoothInterface.getConnections.contains(target))
+ if (!bluetoothInterface.getConnections.contains(target))
return
val header = new MessageHeader(body.Type, MessageHeader.DefaultHopLimit,
- Crypto.getLocalAddress, target, 0, 0)
+ Crypto.getLocalAddress(this), target, 0, 0)
val msg = new Message(header, body)
- val encrypted = Crypto.encrypt(Crypto.sign(msg))
- BluetoothInterface.send(encrypted)
+ val encrypted = crypto.encrypt(crypto.sign(msg))
+ bluetoothInterface.send(encrypted)
onNewMessage(msg)
}
@@ -141,8 +143,8 @@ class ChatService extends Service {
* Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]].
*/
def onMessageReceived(msg: Message): Unit = {
- val decrypted = Crypto.decrypt(msg)
- if (!Crypto.verify(decrypted)) {
+ val decrypted = crypto.decrypt(msg)
+ if (!crypto.verify(decrypted)) {
Log.i(Tag, "Ignoring message with invalid signature from " + msg.Header.Origin)
return
}
@@ -160,6 +162,25 @@ class ChatService extends Service {
Database.changeContactName(contact)
callConnectionListeners()
+ case _: RequestAddContact =>
+ if (msg.Header.Origin == Crypto.getLocalAddress(this))
+ return
+
+ Log.i(Tag, "Remote device " + msg.Header.Origin +
+ " wants to add us as a contact, showing notification")
+ val intent = new Intent(this, classOf[ConfirmAddContactDialog])
+ intent.putExtra(ConfirmAddContactDialog.ExtraContactAddress, msg.Header.Origin.toString)
+ val pi = PendingIntent.getActivity(this, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val notification = new Notification.Builder(this)
+ .setContentTitle(getString(R.string.notification_friend_request, getUser(msg.Header.Origin)))
+ .setSmallIcon(R.drawable.ic_launcher)
+ .setContentIntent(pi)
+ .setAutoCancel(true)
+ .build()
+ val nm = getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
+ nm.notify(notificationIdGenerator.iterator.next(), notification)
case _ =>
MainHandler.post(new Runnable {
override def run(): Unit =
@@ -182,19 +203,19 @@ class ChatService extends Service {
*/
def onConnectionOpened(msg: Message): Boolean = {
val info = msg.Body.asInstanceOf[ConnectionInfo]
- val sender = Crypto.calculateAddress(info.key)
+ val sender = crypto.calculateAddress(info.key)
if (sender == Address.Broadcast || sender == Address.Null) {
Log.i(Tag, "Ignoring ConnectionInfo message with invalid sender " + sender)
false
}
- if (Crypto.havePublicKey(sender) && !Crypto.verify(msg, Crypto.getPublicKey(sender))) {
+ if (crypto.havePublicKey(sender) && !crypto.verify(msg, crypto.getPublicKey(sender))) {
Log.i(Tag, "Ignoring ConnectionInfo message with invalid signature")
false
}
- if (!Crypto.havePublicKey(sender)) {
- Crypto.addPublicKey(sender, info.key)
+ if (!crypto.havePublicKey(sender)) {
+ crypto.addPublicKey(sender, info.key)
Log.i(Tag, "Added public key for new device " + sender.toString)
}
@@ -220,7 +241,7 @@ class ChatService extends Service {
* Returns all direct neighbors.
*/
def getConnections: Set[User] = {
- BluetoothInterface.getConnections.map{ address =>
+ bluetoothInterface.getConnections.map{ address =>
(Database.getContacts ++ connections).find(_.Address == address) match {
case Some(contact) => contact
case None => new User(address, address.toString)
@@ -228,4 +249,7 @@ class ChatService extends Service {
}
}
+ def getUser(address: Address) =
+ getConnections.find(_.Address == address).getOrElse(new User(address, address.toString))
+
}
diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
index d1d7b1c..01ea1d7 100644
--- a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
+++ b/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala
@@ -308,6 +308,4 @@ class Crypto(Context: Context) {
new Address(hash)
}
- def getLocalAddress = Crypto.getLocalAddress(Context)
-
}