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:
parent
e9cfdc0481
commit
0a61af733e
14 changed files with 26 additions and 488 deletions
27
PROTOCOL.md
27
PROTOCOL.md
|
@ -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.
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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" >
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Reference in a new issue