Greatly simplified add contact process.

Removed ConfirmAddContactActivity, only one side has to confirm
now. After that, messages can be sent immediately. The other
side adds the contact when the first message is received.
This commit is contained in:
Felix Ableitner 2015-09-11 15:50:04 +02:00
parent e9cfdc0481
commit 0a61af733e
14 changed files with 26 additions and 488 deletions

View File

@ -229,33 +229,6 @@ contain the Content-Type and Message ID fields.
These messages always have a Protocol-Type of 255. These messages always have a Protocol-Type of 255.
### RequestAddContact (Content-Type = 1)
Sent when a user wants to add another node as a contact. After this,
a ResultAddContact message should be returned.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
### ResultAddContact (Content-Type = 2)
Sent as response to a RequestAddContact 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|A| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Accepted bit (A) is true if the user accepts the new contact, false
otherwise. Nodes should only add another node as a contact if both
users agreed.
### Text (Content-Type = 3) ### Text (Content-Type = 3)
A simple chat message. A simple chat message.

View File

@ -1,18 +0,0 @@
package com.nutomic.ensichat.protocol.body
import android.test.AndroidTestCase
import junit.framework.Assert._
class ResultAddContactTest extends AndroidTestCase {
def testWriteRead(): Unit = {
Array(true, false).foreach { a =>
val rac = new ResultAddContact(a)
val bytes = rac.write
val read = ResultAddContact.read(bytes)
assertEquals(a, read.accepted)
}
}
}

View File

@ -1,52 +0,0 @@
package com.nutomic.ensichat.util
import java.util.GregorianCalendar
import android.test.AndroidTestCase
import com.nutomic.ensichat.protocol.body.{RequestAddContact, ResultAddContact}
import com.nutomic.ensichat.protocol.header.ContentHeader
import com.nutomic.ensichat.protocol.{Address, Crypto, Message, UserTest}
import com.nutomic.ensichat.util.DatabaseTest.DatabaseContext
import junit.framework.Assert._
class AddContactsHandlerTest extends AndroidTestCase {
private lazy val handler =
new AddContactsHandler(context, (address: Address) => UserTest.u1, UserTest.u1.address)
private lazy val context = new DatabaseContext(getContext)
private lazy val database = new Database(context)
private lazy val crypto = new Crypto(getContext)
private lazy val header1 = new ContentHeader(UserTest.u1.address, crypto.localAddress, 0,
RequestAddContact.Type, Some(0), Some(new GregorianCalendar(1970, 1, 1).getTime))
private lazy val header2 = new ContentHeader(UserTest.u1.address, crypto.localAddress, 0,
ResultAddContact.Type, Some(0), Some(new GregorianCalendar(2014, 6, 10).getTime))
private lazy val header3 = new ContentHeader(crypto.localAddress, UserTest.u1.address, 0,
ResultAddContact.Type, Some(0), Some(new GregorianCalendar(2020, 11, 11).getTime))
override def tearDown(): Unit = {
super.tearDown()
database.close()
context.deleteDbFile()
}
def testAddContact(): Unit = {
assertFalse(database.getContact(UserTest.u1.address).isDefined)
handler.onMessageReceived(new Message(header1, new RequestAddContact))
handler.onMessageReceived(new Message(header2, new ResultAddContact(true)))
handler.onMessageReceived(new Message(header3, new ResultAddContact(true)))
assertTrue(database.getContact(UserTest.u1.address).isDefined)
}
def testAddContactDenied(): Unit = {
assertFalse(database.getContact(UserTest.u1.address).isDefined)
handler.onMessageReceived(new Message(header1, new RequestAddContact))
handler.onMessageReceived(new Message(header2, new ResultAddContact(true)))
handler.onMessageReceived(new Message(header3, new ResultAddContact(false)))
assertFalse(database.getContact(UserTest.u1.address).isDefined)
}
}

View File

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

View File

@ -1,62 +0,0 @@
<?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"
android:padding="10dip">
<ScrollView
android:id="@+id/scrollview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/hint">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="10dip">
<TextView
android:id="@+id/local_identicon_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="@string/local_fingerprint_title"/>
<ImageView
android:id="@+id/local_identicon"
android:layout_width="150dip"
android:layout_height="150dip"
android:layout_centerHorizontal="true"
android:layout_below="@id/local_identicon_title"/>
<TextView
android:id="@+id/remote_identicon_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/local_identicon"/>
<ImageView
android:id="@+id/remote_identicon"
android:layout_width="150dip"
android:layout_height="150dip"
android:layout_centerHorizontal="true"
android:layout_below="@id/remote_identicon_title"/>
</RelativeLayout>
</ScrollView>
<TextView
android:id="@+id/hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center"
android:layout_alignParentBottom="true"
android:text="@string/add_contact_dialog_hint"/>
</RelativeLayout>

View File

@ -45,23 +45,11 @@
<!-- Empty text for devices list --> <!-- Empty text for devices list -->
<string name="devices_empty">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>
<!-- ConfirmAddContactDialog --> <!-- Toast shown after contact has been added -->
<string name="toast_contact_added">Contact added</string>
<!-- 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>
<!-- Text to describe the local device's identicon image -->
<string name="local_fingerprint_title">Your key fingerprint:</string>
<!-- Text to describe the remote device's identicon image. Argument is the contact name -->
<string name="remote_fingerprint_title">%1$s\'s key fingerprint:</string>
<!-- 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>
<!-- Toast shown when a new contact was added. Parameter is contact name -->
<string name="contact_added">%1$s was added as a contact</string>
<!-- SettingsActivity --> <!-- SettingsActivity -->
@ -106,9 +94,6 @@
<!-- ChatService --> <!-- ChatService -->
<!-- Notification text for incoming friend request -->
<string name="notification_friend_request">New friend request!</string>
<!-- Notification text for incoming message --> <!-- Notification text for incoming message -->
<string name="notification_message">New message!</string> <string name="notification_message">New message!</string>

View File

@ -1,6 +1,8 @@
package com.nutomic.ensichat.activities package com.nutomic.ensichat.activities
import android.content.{IntentFilter, Intent, Context, BroadcastReceiver} import android.app.AlertDialog.Builder
import android.content.DialogInterface.OnClickListener
import android.content._
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.NavUtils import android.support.v4.app.NavUtils
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
@ -9,7 +11,6 @@ import android.widget.AdapterView.OnItemClickListener
import android.widget._ import android.widget._
import com.nutomic.ensichat.R import com.nutomic.ensichat.R
import com.nutomic.ensichat.protocol.ChatService import com.nutomic.ensichat.protocol.ChatService
import com.nutomic.ensichat.protocol.body.RequestAddContact
import com.nutomic.ensichat.util.Database import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter import com.nutomic.ensichat.views.UsersAdapter
@ -54,10 +55,17 @@ class AddContactsActivity extends EnsichatActivity with OnItemClickListener {
*/ */
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = { override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
val contact = adapter.getItem(position) val contact = adapter.getItem(position)
service.sendTo(contact.address, new RequestAddContact()) new Builder(this)
val intent = new Intent(this, classOf[ConfirmAddContactActivity]) .setMessage(getString(R.string.dialog_add_contact, contact.name))
intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, contact.address.toString) .setPositiveButton(android.R.string.yes, new OnClickListener {
startActivity(intent) 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 { override def onOptionsItemSelected(item: MenuItem): Boolean = item.getItemId match {

View File

@ -1,87 +0,0 @@
package com.nutomic.ensichat.activities
import android.app.AlertDialog
import android.content.DialogInterface.OnClickListener
import android.content._
import android.os.Bundle
import android.support.v4.content.LocalBroadcastManager
import android.view.{ContextThemeWrapper, LayoutInflater}
import android.widget.{ImageView, TextView}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.protocol.body.ResultAddContact
import com.nutomic.ensichat.protocol.{ChatService, Address, Crypto}
import com.nutomic.ensichat.util.IdenticonGenerator
object ConfirmAddContactActivity {
val ExtraContactAddress = "contact_address"
}
/**
* Shows a dialog for adding a new contact (including key fingerprints).
*/
class ConfirmAddContactActivity extends EnsichatActivity with OnClickListener
with DialogInterface.OnDismissListener {
private lazy val user = service.getUser(
new Address(getIntent.getStringExtra(ConfirmAddContactActivity.ExtraContactAddress)))
private lazy val view = getSystemService(Context.LAYOUT_INFLATER_SERVICE)
.asInstanceOf[LayoutInflater]
.inflate(R.layout.dialog_add_contact, null)
private lazy val dialog = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme))
.setTitle(getString(R.string.add_contact_dialog, user.name))
.setView(view)
.setPositiveButton(android.R.string.yes, this)
.setNegativeButton(android.R.string.no, this)
.setOnDismissListener(this)
.create()
override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState)
runOnServiceConnected(() => showDialog())
LocalBroadcastManager.getInstance(this)
.registerReceiver(onConnectionsChangedReceiver,
new IntentFilter(ChatService.ActionConnectionsChanged))
}
override def onDestroy(): Unit = {
super.onDestroy()
LocalBroadcastManager.getInstance(this).unregisterReceiver(onConnectionsChangedReceiver)
}
/**
* Shows a dialog to accept/deny adding a device as a new contact.
*/
private def showDialog(): Unit = {
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]
val localAddress = new Crypto(this).localAddress
local.setImageBitmap(IdenticonGenerator.generate(localAddress, (150, 150), this))
remote.setImageBitmap(IdenticonGenerator.generate(user.address, (150, 150), this))
remoteTitle.setText(getString(R.string.remote_fingerprint_title, user.name))
dialog.show()
}
override def onClick(dialogInterface: DialogInterface, i: Int): Unit =
service.sendTo(user.address, new ResultAddContact(i == DialogInterface.BUTTON_POSITIVE))
override def onDismiss(dialog: DialogInterface): Unit = finish()
private val onConnectionsChangedReceiver = new BroadcastReceiver {
override def onReceive(context: Context, intent: Intent): Unit = {
if (!service.connections().contains(user.address)) {
dialog.dismiss()
service.sendTo(user.address, new ResultAddContact(false))
finish()
}
}
}
}

View File

@ -15,7 +15,7 @@ import com.nutomic.ensichat.bluetooth.BluetoothInterface
import com.nutomic.ensichat.fragments.SettingsFragment import com.nutomic.ensichat.fragments.SettingsFragment
import com.nutomic.ensichat.protocol.body.{ConnectionInfo, MessageBody, UserInfo} import com.nutomic.ensichat.protocol.body.{ConnectionInfo, MessageBody, UserInfo}
import com.nutomic.ensichat.protocol.header.ContentHeader import com.nutomic.ensichat.protocol.header.ContentHeader
import com.nutomic.ensichat.util.{AddContactsHandler, Database, NotificationHandler} import com.nutomic.ensichat.util.{Database, NotificationHandler}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
@ -62,8 +62,6 @@ class ChatService extends Service {
private lazy val notificationHandler = new NotificationHandler(this) private lazy val notificationHandler = new NotificationHandler(this)
private lazy val addContactsHandler = new AddContactsHandler(this, getUser, crypto.localAddress)
private lazy val router = new Router(connections, sendVia) private lazy val router = new Router(connections, sendVia)
private lazy val seqNumGenerator = new SeqNumGenerator(this) private lazy val seqNumGenerator = new SeqNumGenerator(this)
@ -163,9 +161,12 @@ class ChatService extends Service {
callConnectionListeners() callConnectionListeners()
case _ => case _ =>
val origin = msg.header.origin
if (origin != crypto.localAddress && database.getContact(origin).isEmpty)
database.addContact(getUser(origin))
database.onMessageReceived(msg) database.onMessageReceived(msg)
notificationHandler.onMessageReceived(msg) notificationHandler.onMessageReceived(msg)
addContactsHandler.onMessageReceived(msg)
val i = new Intent(ChatService.ActionMessageReceived) val i = new Intent(ChatService.ActionMessageReceived)
i.putExtra(ChatService.ExtraMessage, msg) i.putExtra(ChatService.ExtraMessage, msg)
LocalBroadcastManager.getInstance(this) LocalBroadcastManager.getInstance(this)

View File

@ -254,8 +254,6 @@ class Crypto(context: Context) {
symmetricCipher.init(Cipher.DECRYPT_MODE, key) symmetricCipher.init(Cipher.DECRYPT_MODE, key)
val decrypted = copyThroughCipher(symmetricCipher, msg.body.asInstanceOf[EncryptedBody].data) val decrypted = copyThroughCipher(symmetricCipher, msg.body.asInstanceOf[EncryptedBody].data)
val body = msg.header.asInstanceOf[ContentHeader].contentType match { val body = msg.header.asInstanceOf[ContentHeader].contentType match {
case RequestAddContact.Type => RequestAddContact.read(decrypted)
case ResultAddContact.Type => ResultAddContact.read(decrypted)
case Text.Type => Text.read(decrypted) case Text.Type => Text.read(decrypted)
case UserInfo.Type => UserInfo.read(decrypted) case UserInfo.Type => UserInfo.read(decrypted)
} }

View File

@ -1,34 +0,0 @@
package com.nutomic.ensichat.protocol.body
import java.nio.ByteBuffer
object RequestAddContact {
val Type = 4
/**
* Constructs [[RequestAddContact]] instance from byte array.
*/
def read(array: Array[Byte]): RequestAddContact = {
new RequestAddContact()
}
}
/**
* Sent when the user initiates adding another device as a contact.
*/
case class RequestAddContact() extends MessageBody {
override def protocolType = -1
override def contentType = RequestAddContact.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
b.array()
}
override def length = 4
}

View File

@ -1,41 +0,0 @@
package com.nutomic.ensichat.protocol.body
import java.nio.ByteBuffer
import com.nutomic.ensichat.util.BufferUtils
object ResultAddContact {
val Type = 5
/**
* Constructs [[ResultAddContact]] instance from byte array.
*/
def read(array: Array[Byte]): ResultAddContact = {
val b = ByteBuffer.wrap(array)
val first = BufferUtils.getUnsignedByte(b)
val accepted = (first & 0x80) != 0
new ResultAddContact(accepted)
}
}
/**
* Contains the result of a [[RequestAddContact]] message.
*/
case class ResultAddContact(accepted: Boolean) extends MessageBody {
override def protocolType = -1
override def contentType = ResultAddContact.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedByte(b, if (accepted) 0x80 else 0)
(0 to 1).foreach(_ => BufferUtils.putUnsignedByte(b, 0))
b.array()
}
override def length = 4
}

View File

@ -1,126 +0,0 @@
package com.nutomic.ensichat.util
import android.app.{NotificationManager, PendingIntent}
import android.content.{Context, Intent}
import android.os.{Handler, Looper}
import android.support.v4.app.NotificationCompat
import android.util.Log
import android.widget.Toast
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{ConfirmAddContactActivity, MainActivity}
import com.nutomic.ensichat.protocol.body.{RequestAddContact, ResultAddContact}
import com.nutomic.ensichat.protocol.{Address, Message, User}
/**
* Handles [[RequestAddContact]] and [[ResultAddContact]] messages, adds new contacts.
*
* @param getUser Returns info about a given address.
* @param localAddress Address of the local device.
*/
class AddContactsHandler(context: Context, getUser: (Address) => User, localAddress: Address) {
private val Tag = "AddContactsHandler"
private val notificationIdAddContactGenerator = Stream.from(100).iterator
private lazy val database = new Database(context)
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
private var currentlyAdding = Map[Address, AddContactInfo]()
private case class AddContactInfo(localConfirmed: Boolean, remoteConfirmed: Boolean,
notificationId: Int)
def onMessageReceived(msg: Message): Unit = {
val remote =
if (msg.header.origin == localAddress)
msg.header.target
else
msg.header.origin
msg.body match {
case _: RequestAddContact =>
// Don't show notification if we are already adding the contact.
// Can happen when both users click on each other to add.
if (currentlyAdding.keySet.contains(remote)) {
notificationManager.cancel(currentlyAdding(remote).notificationId)
return
}
// Notification ID is unused for requests coming from local device, but that doesn't matter.
val notificationId = notificationIdAddContactGenerator.next()
currentlyAdding += (remote -> new AddContactInfo(false, false, notificationId))
// Don't show notification for requests coming from local device.
if (msg.header.origin == localAddress)
return
Log.i(Tag, "Remote device " + remote + " wants to add us as a contact")
val intent = new Intent(context, classOf[ConfirmAddContactActivity])
intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, msg.header.origin.toString)
val pi = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
val notification = new NotificationCompat.Builder(context)
.setContentTitle(context.getString(R.string.notification_friend_request, getUser(remote)))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pi)
.setAutoCancel(true)
.build()
notificationManager.notify(notificationId, notification)
case res: ResultAddContact =>
if (!currentlyAdding.contains(remote)) {
Log.w(Tag, "ResultAddContact without previous RequestAddContact, ignoring")
return
}
val previousInfo = currentlyAdding(remote)
val newInfo =
if (msg.header.origin == localAddress)
new AddContactInfo(res.accepted, previousInfo.remoteConfirmed, previousInfo.notificationId)
else
new AddContactInfo(previousInfo.localConfirmed, res.accepted, previousInfo.notificationId)
currentlyAdding += (remote -> newInfo)
if (res.accepted)
addContactIfBothConfirmed(remote)
else {
currentlyAdding -= remote
notificationManager.cancel(previousInfo.notificationId)
}
case _ =>
}
}
/**
* Adds the given address as a new contact, if local and remote device sent a [[ResultAddContact]]
* message with accepted = true.
*/
private def addContactIfBothConfirmed(address: Address): Unit = {
val info = currentlyAdding(address)
val user = getUser(address)
if (info.localConfirmed && info.remoteConfirmed) {
Log.i(Tag, "Adding new contact " + user.toString)
database.addContact(user)
new Handler(Looper.getMainLooper).post(new Runnable {
override def run(): Unit = {
Toast.makeText(context, context.getString(R.string.contact_added, user.name),
Toast.LENGTH_SHORT).show()
}
})
val intent = new Intent(context, classOf[MainActivity])
intent.setAction(MainActivity.ActionOpenChat)
intent.putExtra(MainActivity.ExtraAddress, address.toString)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_SINGLE_TOP)
context.startActivity(intent)
currentlyAdding -= address
notificationManager.cancel(info.notificationId)
}
}
}

View File

@ -2,16 +2,16 @@ package com.nutomic.ensichat.util
import java.util.Date import java.util.Date
import android.content.{Intent, ContentValues, Context} import android.content.{ContentValues, Context, Intent}
import android.database.Cursor import android.database.Cursor
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper} import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
import com.nutomic.ensichat.protocol._ import com.nutomic.ensichat.protocol._
import com.nutomic.ensichat.protocol.body.{Text, ResultAddContact, RequestAddContact} import com.nutomic.ensichat.protocol.body.Text
import com.nutomic.ensichat.protocol.header.ContentHeader import com.nutomic.ensichat.protocol.header.ContentHeader
import scala.collection.SortedSet
import scala.collection.immutable.TreeSet import scala.collection.immutable.TreeSet
import scala.collection.{SortedSet, mutable}
object Database { object Database {
@ -96,8 +96,6 @@ class Database(context: Context)
cv.put("date", msg.header.time.get.getTime.toString) cv.put("date", msg.header.time.get.getTime.toString)
cv.put("text", text.text) cv.put("text", text.text)
getWritableDatabase.insert("messages", null, cv) getWritableDatabase.insert("messages", null, cv)
case _: RequestAddContact | _: ResultAddContact =>
// Never stored.
} }
/** /**