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.
This commit is contained in:
Felix Ableitner 2015-02-01 23:41:41 +01:00
parent 04fd815001
commit 3a90f2d9a3
6 changed files with 196 additions and 151 deletions

View File

@ -34,6 +34,11 @@
android:value=".activities.MainActivity" />
</activity>
<activity
android:name=".activities.ConfirmAddContactDialog"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" />
<activity
android:name=".activities.SettingsActivity"
android:label="@string/settings" >

View File

@ -27,6 +27,12 @@
<!-- Activity title -->
<string name="add_contacts">Add Contacts</string>
<!-- Empty text for devices list -->
<string name="no_devices_nearby">No nearby devices found</string>
<!-- ConfirmAddContactDialog -->
<!-- Title of dialog shown when clicking a device -->
<string name="add_contact_dialog">Do you want to add %1$s as a new contact?</string>
@ -39,15 +45,12 @@
<!-- Information text shown in the "add contact" dialog -->
<string name="add_contact_dialog_hint">Before accepting, make sure the images match on both devices</string>
<!-- Empty text for devices list -->
<string name="no_devices_nearby">No nearby devices found</string>
<!-- Toast shown when a contact is not added because it was not accepted on the other device -->
<string name="contact_not_added">Contact not added (denied by other device)</string>
<!-- Toast shown when a new contact was added. Parameter is contact name -->
<string name="contact_added">%1$s was added as a contact</string>
<!-- Toast shown when a contact is not added because it was not accepted on the other device -->
<string name="contact_not_added">Contact not added (denied by other device)</string>
<!-- SettingsActivity -->
@ -60,4 +63,10 @@
<!-- Preference title -->
<string name="scan_interval_seconds">Scan Interval (seconds)</string>
<!-- ChatService -->
<!-- Notification text for incoming friend request -->
<string name="notification_friend_request">New friend request!</string>
</resources>

View File

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

View File

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

View File

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

View File

@ -308,6 +308,4 @@ class Crypto(Context: Context) {
new Address(hash)
}
def getLocalAddress = Crypto.getLocalAddress(Context)
}