Moved logic for adding new contact to service.

This means the activity doesn't have to be held open.
This commit is contained in:
Felix Ableitner 2015-03-07 16:30:35 +01:00
parent c670588f98
commit 60a8dd59de
9 changed files with 252 additions and 163 deletions

View file

@ -0,0 +1,55 @@
package com.nutomic.ensichat.util
import android.content.Context
import android.test.AndroidTestCase
import com.nutomic.ensichat.protocol.messages._
import com.nutomic.ensichat.protocol.{Address, Crypto, UserTest}
import junit.framework.Assert._
class AddContactsHandlerTest extends AndroidTestCase {
private class MockContext(context: Context) extends DatabaseTest.MockContext(context) {
override def getResources = context.getResources
override def getSystemService(name: String) = context.getSystemService(name)
override def getPackageName = context.getPackageName
}
private lazy val handler =
new AddContactsHandler(context, (address: Address) => UserTest.u1, UserTest.u1.address)
private lazy val context = new MockContext(getContext)
private lazy val database = new Database(context)
private lazy val crypto = new Crypto(getContext)
private lazy val header1 =
new MessageHeader(RequestAddContact.Type, 0, UserTest.u1.address, crypto.localAddress, 0)
private lazy val header2 =
new MessageHeader(ResultAddContact.Type, 0, UserTest.u1.address, crypto.localAddress, 0)
private lazy val header3 =
new MessageHeader(ResultAddContact.Type, 0, crypto.localAddress, UserTest.u1.address, 0)
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

@ -6,28 +6,38 @@ import android.content.Context
import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.test.AndroidTestCase
import android.test.mock.MockContext
import android.test.mock
import com.nutomic.ensichat.protocol.UserTest
import com.nutomic.ensichat.protocol.messages.MessageTest._
import com.nutomic.ensichat.util.Database.OnContactsUpdatedListener
import junit.framework.Assert._
class DatabaseTest extends AndroidTestCase {
object DatabaseTest {
private class TestContext(context: Context) extends MockContext {
/**
* Provides a temporary database file that can be deleted with [[MockContext#deleteDbFile]].
*
* Does not work if multiple db files are opened!
*/
class MockContext(context: Context) extends mock.MockContext {
private var dbFile: String = _
override def openOrCreateDatabase(file: String, mode: Int, factory:
SQLiteDatabase.CursorFactory, errorHandler: DatabaseErrorHandler): SQLiteDatabase = {
dbFile = file + "-test"
context.openOrCreateDatabase(dbFile, mode, factory, errorHandler)
}
def deleteDbFile() = context.deleteDatabase(dbFile)
}
private var dbFile: String = _
}
private var database: Database = _
class DatabaseTest extends AndroidTestCase {
private lazy val context = new DatabaseTest.MockContext(getContext)
private lazy val database = new Database(context)
override def setUp(): Unit = {
database = new Database(new TestContext(getContext))
database.onMessageReceived(m1)
database.onMessageReceived(m2)
database.onMessageReceived(m3)
@ -36,7 +46,7 @@ class DatabaseTest extends AndroidTestCase {
override def tearDown(): Unit = {
super.tearDown()
database.close()
getContext.deleteDatabase(dbFile)
context.deleteDbFile()
}
def testMessageCount(): Unit = {

View file

@ -35,7 +35,7 @@
</activity>
<activity
android:name=".activities.ConfirmAddContactDialog"
android:name=".activities.ConfirmAddContactActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true" />

View file

@ -51,8 +51,8 @@ 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())
val intent = new Intent(this, classOf[ConfirmAddContactDialog])
intent.putExtra(ConfirmAddContactDialog.ExtraContactAddress, contact.address.toString)
val intent = new Intent(this, classOf[ConfirmAddContactActivity])
intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, contact.address.toString)
startActivity(intent)
}

View file

@ -0,0 +1,67 @@
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.{Database, IdenticonGenerator}
object ConfirmAddContactActivity {
val ExtraContactAddress = "contact_address"
}
/**
* Shows a dialog for adding a new contact (including key fingerprints).
*/
class ConfirmAddContactActivity extends EnsiChatActivity with OnClickListener {
private lazy val user = service.getUser(
new Address(getIntent.getStringExtra(ConfirmAddContactActivity.ExtraContactAddress)))
override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState)
runOnServiceConnected(() => showDialog())
}
/**
* 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]
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))
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)
.setCancelable(false)
.show()
}
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
service.sendTo(user.address, new ResultAddContact(i == DialogInterface.BUTTON_POSITIVE))
finish()
}
}

View file

@ -1,127 +0,0 @@
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.{Database, 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 database = new Database(this)
private lazy val user = service.getUser(
new Address(getIntent.getStringExtra(ConfirmAddContactDialog.ExtraContactAddress)))
private var localConfirmed = false
private var remoteConfirmed = false
private lazy val crypto = new Crypto(this)
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.localAddress, (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)
.setCancelable(false)
.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 =>
finish()
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 it was updated in the mean time.
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.localAddress)
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

@ -7,12 +7,12 @@ import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{MainActivity, ConfirmAddContactDialog}
import com.nutomic.ensichat.activities.{MainActivity, ConfirmAddContactActivity}
import com.nutomic.ensichat.bluetooth.BluetoothInterface
import com.nutomic.ensichat.fragments.SettingsFragment
import com.nutomic.ensichat.protocol.ChatService.{OnConnectionsChangedListener, OnMessageReceivedListener}
import com.nutomic.ensichat.protocol.messages._
import com.nutomic.ensichat.util.{NotificationHandler, Database}
import com.nutomic.ensichat.util.{AddContactsHandler, NotificationHandler, Database}
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
@ -64,12 +64,12 @@ class ChatService extends Service {
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 seqNumGenerator = new SeqNumGenerator(this)
private val notificationIdAddContactGenerator = Stream.from(100).iterator
/**
* For this (and [[messageListeners]], functions would be useful instead of instances,
* but on a Nexus S (Android 4.1.2), these functions are garbage collected even when
@ -97,11 +97,11 @@ class ChatService extends Service {
pm.edit().putString(SettingsFragment.KeyUserName,
BluetoothAdapter.getDefaultAdapter.getName).apply()
registerMessageListener(database)
registerMessageListener(notificationHandler)
Future {
crypto.generateLocalKeys()
registerMessageListener(database)
registerMessageListener(notificationHandler)
registerMessageListener(addContactsHandler)
btInterface.create()
Log.i(Tag, "Service started, address is " + crypto.localAddress)
@ -177,25 +177,6 @@ class ChatService extends Service {
database.changeContactName(contact)
callConnectionListeners()
case _: RequestAddContact =>
if (msg.Header.origin == crypto.localAddress)
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(notificationIdAddContactGenerator.next(), notification)
case _ =>
mainHandler.post(new Runnable {
override def run(): Unit =

View file

@ -0,0 +1,102 @@
package com.nutomic.ensichat.util
import android.app.{Notification, NotificationManager, PendingIntent}
import android.content.{Context, Intent}
import android.util.Log
import android.widget.Toast
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.ConfirmAddContactActivity
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.protocol.messages.{RequestAddContact, Message, ResultAddContact}
import com.nutomic.ensichat.protocol.{Address, 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)
extends OnMessageReceivedListener {
private val Tag = "AddContactsHandler"
private val notificationIdAddContactGenerator = Stream.from(100).iterator
private lazy val database = new Database(context)
private var currentlyAdding = Map[Address, AddContactInfo]()
private case class AddContactInfo(localConfirmed: Boolean, remoteConfirmed: Boolean)
def onMessageReceived(msg: Message): Unit = {
val remote =
if (msg.Header.origin == localAddress)
msg.Header.target
else
msg.Header.origin
msg.Body match {
case _: RequestAddContact =>
Log.i(Tag, "Remote device " + remote + " wants to add us as a contact")
currentlyAdding += (remote -> new AddContactInfo(false, false))
// Don't show notification for requests coming from local device.
if (msg.Header.origin == localAddress)
return
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 Notification.Builder(context)
.setContentTitle(context.getString(R.string.notification_friend_request, getUser(remote)))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pi)
.setAutoCancel(true)
.build()
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
nm.notify(notificationIdAddContactGenerator.next(), notification)
case res: ResultAddContact =>
if (!currentlyAdding.contains(remote)) {
Log.w(Tag, "ResultAddContact without previous RequestAddContact, ignoring")
return
}
val newInfo =
if (msg.Header.origin == localAddress)
new AddContactInfo(res.accepted, currentlyAdding(remote).remoteConfirmed)
else
new AddContactInfo(currentlyAdding(remote).localConfirmed, res.accepted)
currentlyAdding += (remote -> newInfo)
if (res.accepted)
addContactIfBothConfirmed(remote)
else {
Toast.makeText(context, R.string.contact_not_added, Toast.LENGTH_LONG).show()
currentlyAdding -= remote
}
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)
Toast
.makeText(context, context.getString(R.string.contact_added, user.name), Toast.LENGTH_SHORT)
.show()
currentlyAdding -= address
}
}
}

View file

@ -32,6 +32,7 @@ class NotificationHandler(context: Context) extends OnMessageReceivedListener {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE)
.asInstanceOf[NotificationManager]
nm.notify(notificationIdNewMessage, notification)
case _ =>
}
}