From 3a90f2d9a365ada335b52946a8f7eb0cecb4a292 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Sun, 1 Feb 2015 23:41:41 +0100 Subject: [PATCH] Use notification and seperate activity to add contact. This might cause problems if the activity is closed after choosing yes, and before the other user confirms. We should probably store the information in the service. --- app/src/main/AndroidManifest.xml | 5 + app/src/main/res/values/strings.xml | 19 ++- .../activities/AddContactsActivity.scala | 132 ++---------------- .../activities/ConfirmAddContactDialog.scala | 121 ++++++++++++++++ .../ensichat/protocol/ChatService.scala | 68 ++++++--- .../nutomic/ensichat/protocol/Crypto.scala | 2 - 6 files changed, 196 insertions(+), 151 deletions(-) create mode 100644 app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactDialog.scala 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) - }