Added functionality to add contacts (with new Activity).
This requires confirmation from both devices involved, and allows opening the ChatFragment for a device that is not currently connected.
This commit is contained in:
parent
8a1e9b4d5d
commit
b12af56ea7
24 changed files with 545 additions and 199 deletions
|
@ -8,9 +8,10 @@ import android.database.sqlite.SQLiteDatabase
|
|||
import android.test.AndroidTestCase
|
||||
import android.test.mock.MockContext
|
||||
import com.nutomic.ensichat.messages.MessageTest._
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import junit.framework.Assert._
|
||||
|
||||
class MessageStoreTest extends AndroidTestCase {
|
||||
class DatabaseTest extends AndroidTestCase {
|
||||
|
||||
private class TestContext(context: Context) extends MockContext {
|
||||
override def openOrCreateDatabase(file: String, mode: Int, factory:
|
||||
|
@ -22,10 +23,10 @@ class MessageStoreTest extends AndroidTestCase {
|
|||
|
||||
private var dbFile: String = _
|
||||
|
||||
private var MessageStore: MessageStore = _
|
||||
private var MessageStore: Database = _
|
||||
|
||||
override def setUp(): Unit = {
|
||||
MessageStore = new MessageStore(new TestContext(getContext))
|
||||
MessageStore = new Database(new TestContext(getContext))
|
||||
MessageStore.addMessage(m1)
|
||||
MessageStore.addMessage(m2)
|
||||
MessageStore.addMessage(m3)
|
|
@ -23,6 +23,11 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.AddContactsActivity"
|
||||
android:label="@string/add_contacts"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
|
|
BIN
app/src/main/res/drawable-hdpi/ic_action_add_person.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_action_add_person.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 616 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_add_person.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_action_add_person.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 469 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_add_person.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_action_add_person.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 798 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_add_person.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_action_add_person.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
18
app/src/main/res/layout/activity_add_contacts.xml
Normal file
18
app/src/main/res/layout/activity_add_contacts.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_devices_nearby"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/add_contact"
|
||||
android:title="@string/add_contacts"
|
||||
android:icon="@drawable/ic_action_add_person"
|
||||
android:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_action_settings"
|
||||
|
@ -10,6 +16,6 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/exit"
|
||||
android:title="Exit" />
|
||||
android:title="@string/exit" />
|
||||
|
||||
</menu>
|
|
@ -11,14 +11,41 @@
|
|||
<!-- Toast shown if user denies request to enable bluetooth -->
|
||||
<string name="bluetooth_required">Bluetooth is required for this app.</string>
|
||||
|
||||
<!-- Menu item to close app and stop service -->
|
||||
<string name="exit">Exit</string>
|
||||
|
||||
|
||||
<!-- ContactsFragment -->
|
||||
|
||||
<!-- Empty text for contacts list -->
|
||||
<string name="no_contacts_found">No contacts found :(</string>
|
||||
<string name="no_contacts_found">You haven\'t added any contacts yet</string>
|
||||
|
||||
<!-- Menu item to close app and stop service -->
|
||||
<string name="exit">Exit</string>
|
||||
|
||||
|
||||
<!-- ChatFragment -->
|
||||
|
||||
<!-- Toast shown when trying to send a message to an offline contact -->
|
||||
<string name="contact_offline_toast">Contact is offline, message not sent</string>
|
||||
|
||||
|
||||
<!-- AddContactsActivity -->
|
||||
|
||||
<!-- Activity title -->
|
||||
<string name="add_contacts">Add Contacts</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>
|
||||
|
||||
<!-- Empty text for devices list -->
|
||||
<string name="no_devices_nearby">No nearby devices found</string>
|
||||
|
||||
<!-- Toast shown when trying to add a contact again -->
|
||||
<string name="contact_already_added">You already added this contact</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 -->
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.os.Bundle
|
||||
import android.view._
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.{AdapterView, ListView, Toast}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, Device}
|
||||
import com.nutomic.ensichat.messages.{Message, RequestAddContactMessage, ResultAddContactMessage}
|
||||
import com.nutomic.ensichat.util.DevicesAdapter
|
||||
|
||||
import scala.collection.SortedSet
|
||||
|
||||
/**
|
||||
* Lists all nearby, connected devices and allows adding them to contacts.
|
||||
*
|
||||
* Adding a contact requires confirmation on both sides.
|
||||
*/
|
||||
class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnectionChangedListener
|
||||
with OnItemClickListener with OnMessageReceivedListener {
|
||||
|
||||
private lazy val Adapter = new DevicesAdapter(this)
|
||||
|
||||
private lazy val Database = service.database
|
||||
|
||||
/**
|
||||
* Map of devices that should be added.
|
||||
*/
|
||||
private var currentlyAdding = Map[Device.ID, 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 class AddContactInfo(val localConfirmed: Boolean, val remoteConfirmed: Boolean) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes layout, registers connection and message listeners.
|
||||
*/
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_add_contacts)
|
||||
val list = findViewById(android.R.id.list).asInstanceOf[ListView]
|
||||
list.setAdapter(Adapter)
|
||||
list.setOnItemClickListener(this)
|
||||
|
||||
runOnServiceConnected(() => {
|
||||
service.registerConnectionListener(AddContactsActivity.this)
|
||||
service.registerMessageListener(this)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays newly connected devices in the list.
|
||||
*/
|
||||
override def onConnectionChanged(devices: Map[Device.ID, Device]): Unit = {
|
||||
val filtered = devices.filter{ case (_, d) => d.Connected }
|
||||
runOnUiThread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
Adapter.clear()
|
||||
filtered.values.foreach(f => Adapter.add(f))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates adding the device as contact if it hasn't been added yet.
|
||||
*/
|
||||
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
|
||||
val device = Adapter.getItem(position)
|
||||
if (Database.isContact(device.Id)) {
|
||||
Toast.makeText(this, R.string.contact_already_added, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
service.send(new RequestAddContactMessage(service.localDeviceId, device.Id, new Date()))
|
||||
addDeviceDialog(device)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog to accept/deny adding a device as a new contact.
|
||||
*/
|
||||
private def addDeviceDialog(device: Device): Unit = {
|
||||
val id = device.Id
|
||||
// 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 += (id -> new AddContactInfo(currentlyAdding(id).localConfirmed, true))
|
||||
addContactIfBothConfirmed(device)
|
||||
service.send(
|
||||
new ResultAddContactMessage(service.localDeviceId, device.Id, new Date(), true))
|
||||
case DialogInterface.BUTTON_NEGATIVE =>
|
||||
// Local user denied adding contact, send info to other device.
|
||||
service.send(
|
||||
new ResultAddContactMessage(service.localDeviceId, device.Id, new Date(), false))
|
||||
}
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.add_contact_dialog, device.Name))
|
||||
.setPositiveButton(android.R.string.yes, onClick)
|
||||
.setNegativeButton(android.R.string.no, onClick)
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming [[RequestAddContactMessage]] and [[ResultAddContactMessage]] 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(messages: SortedSet[Message]): Unit = {
|
||||
messages.foreach(m => {
|
||||
if (m.receiver == service.localDeviceId) {
|
||||
m.messageType match {
|
||||
case Message.Type.RequestAddContact =>
|
||||
// Remote device wants to add us as a contact, show dialog.
|
||||
val sender = getDevice(m.sender)
|
||||
addDeviceDialog(sender)
|
||||
case Message.Type.ResultAddContact =>
|
||||
if (m.asInstanceOf[ResultAddContactMessage].Accepted) {
|
||||
// Remote device accepted us as a contact, update state.
|
||||
currentlyAdding += (m.sender ->
|
||||
new AddContactInfo(true, currentlyAdding(m.sender).remoteConfirmed))
|
||||
addContactIfBothConfirmed(getDevice(m.sender))
|
||||
} else {
|
||||
// Remote device denied us as a contact, show a toast
|
||||
// and remove from [[currentlyAdding]].
|
||||
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
||||
currentlyAdding -= m.sender
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [[Device]] for a given [[Device.ID]] that is stored in the [[Adapter]].
|
||||
*/
|
||||
private def getDevice(id: Device.ID): Device = {
|
||||
// ArrayAdapter does not return the underlying array so we have to access it manually.
|
||||
for (i <- 0 until Adapter.getCount) {
|
||||
if (Adapter.getItem(i).Id == id) {
|
||||
return Adapter.getItem(i)
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Device to add was not found")
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given device to contacts if [[AddContactInfo.localConfirmed]] and
|
||||
* [[AddContactInfo.remoteConfirmed]] are true for it in [[currentlyAdding]].
|
||||
*/
|
||||
private def addContactIfBothConfirmed(device: Device): Unit = {
|
||||
val info = currentlyAdding(device.Id)
|
||||
if (info.localConfirmed && info.remoteConfirmed) {
|
||||
Database.addContact(device)
|
||||
Toast.makeText(this, getString(R.string.contact_added, device.Name), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
currentlyAdding -= device.Id
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@ import android.content._
|
|||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, Device}
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.fragments.{ChatFragment, ContactsFragment}
|
||||
|
||||
/**
|
||||
|
@ -89,7 +89,7 @@ class MainActivity extends EnsiChatActivity {
|
|||
if (currentChat != None) {
|
||||
getFragmentManager
|
||||
.beginTransaction()
|
||||
.remove(getFragmentManager().findFragmentById(android.R.id.content))
|
||||
.remove(getFragmentManager.findFragmentById(android.R.id.content))
|
||||
.attach(ContactsFragment)
|
||||
.commit()
|
||||
currentChat = None
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.nutomic.ensichat.bluetooth
|
||||
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.{Date, UUID}
|
||||
|
||||
import android.app.Service
|
||||
|
@ -8,12 +9,13 @@ import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
|||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.bluetooth.ChatService.{OnConnectionChangedListener, OnMessageReceivedListener}
|
||||
import com.nutomic.ensichat.messages._
|
||||
import com.nutomic.ensichat.util.Database
|
||||
import com.nutomic.ensichat.{BuildConfig, R}
|
||||
|
||||
import scala.collection.SortedSet
|
||||
import scala.collection.immutable.{HashMap, HashSet, TreeSet}
|
||||
import scala.collection.{SortedSet, mutable}
|
||||
import scala.ref.WeakReference
|
||||
|
||||
object ChatService {
|
||||
|
@ -53,9 +55,7 @@ class ChatService extends Service {
|
|||
*/
|
||||
private var connectionListeners = new HashSet[WeakReference[OnConnectionChangedListener]]()
|
||||
|
||||
private val messageListeners =
|
||||
mutable.HashMap[Device.ID, mutable.Set[WeakReference[OnMessageReceivedListener]]]()
|
||||
.withDefaultValue(mutable.Set[WeakReference[OnMessageReceivedListener]]())
|
||||
private var messageListeners = Set[WeakReference[OnMessageReceivedListener]]()
|
||||
|
||||
private var devices = new HashMap[Device.ID, Device]()
|
||||
|
||||
|
@ -67,7 +67,7 @@ class ChatService extends Service {
|
|||
|
||||
private val MainHandler = new Handler()
|
||||
|
||||
private var MessageStore: MessageStore = _
|
||||
private lazy val Database = new Database(this)
|
||||
|
||||
private lazy val Crypto = new Crypto(getFilesDir)
|
||||
|
||||
|
@ -77,8 +77,6 @@ class ChatService extends Service {
|
|||
override def onCreate(): Unit = {
|
||||
super.onCreate()
|
||||
|
||||
MessageStore = new MessageStore(this)
|
||||
|
||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter
|
||||
|
||||
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
|
||||
|
@ -137,7 +135,7 @@ class ChatService extends Service {
|
|||
override def onReceive(context: Context, intent: Intent) {
|
||||
val device: Device =
|
||||
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
|
||||
devices += (device.id -> device)
|
||||
devices += (device.Id -> device)
|
||||
new ConnectThread(device, onConnectionChanged).start()
|
||||
}
|
||||
}
|
||||
|
@ -191,13 +189,14 @@ class ChatService extends Service {
|
|||
* @param socket A socket for data transfer if device.connected is true, otherwise null.
|
||||
*/
|
||||
def onConnectionChanged(device: Device, socket: BluetoothSocket): Unit = {
|
||||
devices += (device.id -> device)
|
||||
devices += (device.Id -> device)
|
||||
|
||||
if (device.connected) {
|
||||
connections += (device.id ->
|
||||
if (device.Connected) {
|
||||
connections += (device.Id ->
|
||||
new TransferThread(device, socket, this, Crypto, handleNewMessage))
|
||||
connections(device.id).start()
|
||||
send(new DeviceInfoMessage(localDeviceId, device.id, new Date(), Crypto.getLocalPublicKey))
|
||||
connections(device.Id).start()
|
||||
connections.apply(device.Id).send(
|
||||
new DeviceInfoMessage(localDeviceId, device.Id, new Date(), Crypto.getLocalPublicKey))
|
||||
}
|
||||
|
||||
connectionListeners.foreach(l => l.get match {
|
||||
|
@ -210,6 +209,9 @@ class ChatService extends Service {
|
|||
* Sends message to the device specified as receiver,
|
||||
*/
|
||||
def send(message: Message): Unit = {
|
||||
if (BuildConfig.DEBUG && message.sender != localDeviceId) {
|
||||
throw new InvalidParameterException("Message must be sent from local device")
|
||||
}
|
||||
connections.apply(message.receiver).send(message)
|
||||
handleNewMessage(message)
|
||||
}
|
||||
|
@ -218,16 +220,22 @@ class ChatService extends Service {
|
|||
* Saves the message to database and sends it to registered listeners.
|
||||
*
|
||||
* If you want to send a new message, use [[send]].
|
||||
*
|
||||
* Messages must always be sent between local device and a contact.
|
||||
*/
|
||||
private def handleNewMessage(message: Message): Unit = {
|
||||
MessageStore.addMessage(message)
|
||||
if (BuildConfig.DEBUG && message.sender != localDeviceId && message.receiver != localDeviceId) {
|
||||
throw new InvalidParameterException("Message must be sent or received by local device")
|
||||
}
|
||||
|
||||
Database.addMessage(message)
|
||||
MainHandler.post(new Runnable {
|
||||
override def run(): Unit = {
|
||||
messageListeners(message.sender).foreach(l =>
|
||||
messageListeners.foreach(l =>
|
||||
if (l.get != null)
|
||||
l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
|
||||
else
|
||||
messageListeners(message.sender) -= l)
|
||||
messageListeners -= l)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -235,9 +243,8 @@ class ChatService extends Service {
|
|||
/**
|
||||
* Registers a listener that is called whenever a new message is sent or received.
|
||||
*/
|
||||
def registerMessageListener(device: Device.ID, listener: OnMessageReceivedListener): Unit = {
|
||||
messageListeners(device) += new WeakReference[OnMessageReceivedListener](listener)
|
||||
listener.onMessageReceived(MessageStore.getMessages(device, 10))
|
||||
def registerMessageListener(listener: OnMessageReceivedListener): Unit = {
|
||||
messageListeners += new WeakReference[OnMessageReceivedListener](listener)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -245,4 +252,8 @@ class ChatService extends Service {
|
|||
*/
|
||||
def localDeviceId = new Device.ID(bluetoothAdapter.getAddress)
|
||||
|
||||
def isConnected(device: Device.ID): Boolean = connections.keySet.contains(device)
|
||||
|
||||
def database = Database
|
||||
|
||||
}
|
|
@ -32,7 +32,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
|
|||
return
|
||||
}
|
||||
|
||||
Log.i(Tag, "Successfully connected to device " + device.name)
|
||||
Log.i(Tag, "Successfully connected to device " + device.Name)
|
||||
onConnected(new Device(device.bluetoothDevice, true), Socket)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,16 +28,13 @@ object Device {
|
|||
/**
|
||||
* Holds information about a remote bluetooth device.
|
||||
*/
|
||||
class Device(BluetoothDevice: BluetoothDevice, Connected: Boolean) {
|
||||
class Device(val Id: Device.ID, val Name: String, val Connected: Boolean,
|
||||
btDevice: Option[BluetoothDevice] = None) {
|
||||
|
||||
def id = new Device.ID(bluetoothDevice.getAddress)
|
||||
def this(btDevice: BluetoothDevice, connected: Boolean) {
|
||||
this(new Device.ID(btDevice.getAddress), btDevice.getName, connected, Option(btDevice))
|
||||
}
|
||||
|
||||
def name = BluetoothDevice.getName
|
||||
|
||||
def connected = Connected
|
||||
|
||||
def bluetoothDevice = BluetoothDevice
|
||||
|
||||
override def toString = "Device(" + name + ", " + bluetoothDevice.getAddress + ")"
|
||||
def bluetoothDevice = btDevice.get
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import java.io._
|
|||
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMessage}
|
||||
import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message}
|
||||
import org.msgpack.ScalaMessagePack
|
||||
|
||||
/**
|
||||
|
@ -22,16 +22,6 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
|
|||
|
||||
private val Tag: String = "TransferThread"
|
||||
|
||||
/**
|
||||
* First value in a message, indicates that content is not encrypted.
|
||||
*/
|
||||
private val MessageUnencrypted = false
|
||||
|
||||
/**
|
||||
* First value in a message, indicates that content is encrypted.
|
||||
*/
|
||||
private val MessageEncrypted = true
|
||||
|
||||
val InStream: InputStream =
|
||||
try {
|
||||
socket.getInputStream
|
||||
|
@ -56,53 +46,54 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
|
|||
while (socket.isConnected) {
|
||||
try {
|
||||
val up = new ScalaMessagePack().createUnpacker(InStream)
|
||||
val plain = up.readBoolean() match {
|
||||
case MessageEncrypted =>
|
||||
val isEncrypted = up.readBoolean()
|
||||
val plain =
|
||||
if (isEncrypted) {
|
||||
val encrypted = up.readByteArray()
|
||||
val key = up.readByteArray()
|
||||
crypto.decrypt(encrypted, key)
|
||||
case MessageUnencrypted =>
|
||||
} else {
|
||||
up.readByteArray()
|
||||
}
|
||||
}
|
||||
val (message, signature) = Message.read(plain)
|
||||
var messageValid = true
|
||||
|
||||
if (message.sender != device.id) {
|
||||
Log.i(Tag, "Dropping message with invalid sender from " + device.id)
|
||||
if (message.sender != device.Id) {
|
||||
Log.i(Tag, "Dropping message with invalid sender from " + device.Id)
|
||||
messageValid = false
|
||||
}
|
||||
|
||||
if (message.receiver != service.localDeviceId) {
|
||||
Log.i(Tag, "Dropping message with different receiver from " + device.id)
|
||||
Log.i(Tag, "Dropping message with different receiver from " + device.Id)
|
||||
messageValid = false
|
||||
}
|
||||
|
||||
// Add public key for new, directly connected device.
|
||||
// Explicitly check that message was not forwarded or spoofed.
|
||||
if (message.isInstanceOf[DeviceInfoMessage] && !crypto.havePublicKey(message.sender) &&
|
||||
message.sender == device.id) {
|
||||
message.sender == device.Id) {
|
||||
val dim = message.asInstanceOf[DeviceInfoMessage]
|
||||
// Permanently store public key for new local devices (also check signature).
|
||||
if (crypto.isValidSignature(message, signature, dim.publicKey)) {
|
||||
crypto.addPublicKey(device.id, dim.publicKey)
|
||||
Log.i(Tag, "Added public key for new device " + device.name)
|
||||
crypto.addPublicKey(device.Id, dim.publicKey)
|
||||
Log.i(Tag, "Added public key for new device " + device.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if (!crypto.isValidSignature(message, signature)) {
|
||||
Log.i(Tag, "Dropping message with invalid signature from " + device.id)
|
||||
Log.i(Tag, "Dropping message with invalid signature from " + device.Id)
|
||||
messageValid = false
|
||||
}
|
||||
|
||||
if (messageValid) {
|
||||
message match {
|
||||
case m: TextMessage => onReceive(m)
|
||||
case m: DeviceInfoMessage => crypto.addPublicKey(message.sender, m.publicKey)
|
||||
case _ => onReceive(message)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
Log.w(Tag, "Connection to " + device.name + " closed with exception", e)
|
||||
Log.w(Tag, "Connection to " + device.Name + " closed with exception", e)
|
||||
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
|
||||
return
|
||||
}
|
||||
|
@ -118,11 +109,13 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
|
|||
message.messageType match {
|
||||
case Message.Type.Text =>
|
||||
val (encrypted, key) = crypto.encrypt(message.receiver, plain)
|
||||
packer.write(MessageEncrypted)
|
||||
// Message is encrypted.
|
||||
packer.write(true)
|
||||
.write(encrypted)
|
||||
.write(key)
|
||||
case Message.Type.DeviceInfo =>
|
||||
packer.write(MessageUnencrypted)
|
||||
case _ =>
|
||||
// Message is not encrypted.
|
||||
packer.write(false)
|
||||
.write(plain)
|
||||
}
|
||||
} catch {
|
||||
|
|
|
@ -3,8 +3,7 @@ package com.nutomic.ensichat.fragments
|
|||
import java.util.Date
|
||||
|
||||
import android.app.ListFragment
|
||||
import android.content.{ComponentName, Context, Intent, ServiceConnection}
|
||||
import android.os.{Bundle, IBinder}
|
||||
import android.os.Bundle
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.{KeyEvent, LayoutInflater, View, ViewGroup}
|
||||
|
@ -13,7 +12,7 @@ import android.widget._
|
|||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.EnsiChatActivity
|
||||
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, Device}
|
||||
import com.nutomic.ensichat.messages.{Message, TextMessage}
|
||||
import com.nutomic.ensichat.util.MessagesAdapter
|
||||
|
||||
|
@ -51,7 +50,8 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
|
||||
// Read local device ID from service,
|
||||
adapter = new MessagesAdapter(getActivity, chatService.localDeviceId)
|
||||
chatService.registerMessageListener(device, ChatFragment.this)
|
||||
chatService.registerMessageListener(ChatFragment.this)
|
||||
onMessageReceived(chatService.database.getMessages(device, 10))
|
||||
|
||||
if (listView != null) {
|
||||
listView.setAdapter(adapter)
|
||||
|
@ -100,6 +100,10 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
case R.id.send =>
|
||||
val text: String = messageText.getText.toString
|
||||
if (!text.isEmpty) {
|
||||
if (!chatService.isConnected(device)) {
|
||||
Toast.makeText(getActivity, R.string.contact_offline_toast, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
chatService.send(
|
||||
new TextMessage(chatService.localDeviceId, device, new Date(), text.toString))
|
||||
messageText.getText.clear()
|
||||
|
@ -111,7 +115,8 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
* Displays new messages in UI.
|
||||
*/
|
||||
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
||||
messages.filter(_.isInstanceOf[TextMessage])
|
||||
messages.filter(m => m.sender == device || m.receiver == device)
|
||||
.filter(_.isInstanceOf[TextMessage])
|
||||
.foreach(m => adapter.add(m.asInstanceOf[TextMessage]))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,42 +1,43 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
import android.app.ListFragment
|
||||
import android.content.{ComponentName, Context, Intent, ServiceConnection}
|
||||
import android.os.{Bundle, IBinder}
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view._
|
||||
import android.widget.{ArrayAdapter, ListView}
|
||||
import android.widget.ListView
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.{SettingsActivity, EnsiChatActivity, MainActivity}
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
|
||||
import com.nutomic.ensichat.util.{MessagesAdapter, DevicesAdapter}
|
||||
import com.nutomic.ensichat.activities.{AddContactsActivity, EnsiChatActivity, MainActivity, SettingsActivity}
|
||||
import com.nutomic.ensichat.bluetooth.ChatService
|
||||
import com.nutomic.ensichat.util.DevicesAdapter
|
||||
|
||||
/**
|
||||
* Lists all nearby, connected devices.
|
||||
*/
|
||||
class ContactsFragment extends ListFragment with ChatService.OnConnectionChangedListener {
|
||||
class ContactsFragment extends ListFragment {
|
||||
|
||||
private lazy val adapter = new DevicesAdapter(getActivity)
|
||||
private lazy val Adapter = new DevicesAdapter(getActivity)
|
||||
|
||||
override def onActivityCreated(savedInstanceState: Bundle): Unit = {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
val activity = getActivity.asInstanceOf[EnsiChatActivity]
|
||||
activity.runOnServiceConnected(() => {
|
||||
activity.service.registerConnectionListener(ContactsFragment.this)
|
||||
})
|
||||
}
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
savedInstanceState: Bundle): View =
|
||||
inflater.inflate(R.layout.fragment_contacts, container, false)
|
||||
private lazy val Database = getActivity.asInstanceOf[EnsiChatActivity].service.database
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setListAdapter(adapter)
|
||||
setListAdapter(Adapter)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
getActivity.asInstanceOf[EnsiChatActivity].runOnServiceConnected(() => {
|
||||
Database.getContacts.foreach(Adapter.add)
|
||||
Database.runOnContactsUpdated(() => {
|
||||
Adapter.clear()
|
||||
Database.getContacts.foreach(Adapter.add)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
savedInstanceState: Bundle): View =
|
||||
inflater.inflate(R.layout.fragment_contacts, container, false)
|
||||
|
||||
override def onCreateOptionsMenu(menu: Menu, inflater: MenuInflater): Unit = {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.main, menu)
|
||||
|
@ -44,6 +45,9 @@ class ContactsFragment extends ListFragment with ChatService.OnConnectionChanged
|
|||
|
||||
override def onOptionsItemSelected(item: MenuItem): Boolean = {
|
||||
item.getItemId match {
|
||||
case R.id.add_contact =>
|
||||
startActivity(new Intent(getActivity, classOf[AddContactsActivity]))
|
||||
true
|
||||
case R.id.settings =>
|
||||
startActivity(new Intent(getActivity, classOf[SettingsActivity]))
|
||||
true
|
||||
|
@ -56,26 +60,10 @@ class ContactsFragment extends ListFragment with ChatService.OnConnectionChanged
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays newly connected devices in the list.
|
||||
*/
|
||||
override def onConnectionChanged(devices: Map[Device.ID, Device]): Unit = {
|
||||
if (getActivity == null)
|
||||
return
|
||||
|
||||
val filtered = devices.filter{ case (_, d) => d.connected }
|
||||
getActivity.runOnUiThread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
adapter.clear()
|
||||
filtered.values.foreach(f => adapter.add(f))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a chat with the clicked device.
|
||||
*/
|
||||
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit =
|
||||
getActivity.asInstanceOf[MainActivity].openChat(adapter.getItem(position).id)
|
||||
getActivity.asInstanceOf[MainActivity].openChat(Adapter.getItem(position).Id)
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package com.nutomic.ensichat.fragments
|
|||
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceFragment
|
||||
|
||||
import com.nutomic.ensichat.R
|
||||
|
||||
class SettingsFragment extends PreferenceFragment {
|
||||
|
|
|
@ -11,11 +11,13 @@ object Message {
|
|||
/**
|
||||
* Types of messages that can be transfered.
|
||||
*
|
||||
* There must be one type for each implementation.
|
||||
* There must be one type for each implementation and vice versa.
|
||||
*/
|
||||
object Type {
|
||||
val Text = 1
|
||||
val DeviceInfo = 2
|
||||
val RequestAddContact = 3
|
||||
val ResultAddContact = 4
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,8 +42,10 @@ object Message {
|
|||
val date = new Date(up.readLong())
|
||||
val sig = up.readByteArray()
|
||||
(messageType match {
|
||||
case Type.Text => TextMessage.read(sender, receiver, date, up)
|
||||
case Type.DeviceInfo => DeviceInfoMessage.read(sender, receiver, date, up)
|
||||
case Type.Text => TextMessage.read(sender, receiver, date, up)
|
||||
case Type.DeviceInfo => DeviceInfoMessage.read(sender, receiver, date, up)
|
||||
case Type.RequestAddContact => RequestAddContactMessage.read(sender, receiver, date, up)
|
||||
case Type.ResultAddContact => ResultAddContactMessage.read(sender, receiver, date, up)
|
||||
}, sig)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package com.nutomic.ensichat.messages
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import android.content.{ContentValues, Context}
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
|
||||
import scala.collection.SortedSet
|
||||
import scala.collection.immutable.TreeSet
|
||||
|
||||
object MessageStore {
|
||||
|
||||
private val DatabaseName = "message_store.db"
|
||||
|
||||
private val DatabaseVersion = 1
|
||||
|
||||
private val DatabaseCreate = "CREATE TABLE messages(" +
|
||||
"_id integer primary key autoincrement," +
|
||||
"sender string not null," +
|
||||
"receiver string not null," +
|
||||
"text blob not null," +
|
||||
"date integer not null);" // Unix timestamp of message.
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all messages in SQL database.
|
||||
*/
|
||||
class MessageStore(context: Context) extends SQLiteOpenHelper(context, MessageStore.DatabaseName,
|
||||
null, MessageStore.DatabaseVersion) {
|
||||
|
||||
private val Tag = "MessageStore"
|
||||
|
||||
override def onCreate(db: SQLiteDatabase): Unit = {
|
||||
db.execSQL(MessageStore.DatabaseCreate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count last messages for device.
|
||||
*/
|
||||
def getMessages(device: Device.ID, count: Int): SortedSet[Message] = {
|
||||
val c: Cursor = getReadableDatabase.query(true,
|
||||
"messages", Array("sender", "receiver", "text", "date"),
|
||||
"sender = ? OR receiver = ?", Array(device.toString, device.toString),
|
||||
null, null, "date DESC", count.toString)
|
||||
var messages: SortedSet[Message] = new TreeSet[Message]()(Message.Ordering)
|
||||
while (c.moveToNext()) {
|
||||
val m: TextMessage = new TextMessage(
|
||||
new Device.ID(c.getString(c.getColumnIndex("sender"))),
|
||||
new Device.ID(c.getString(c.getColumnIndex("receiver"))),
|
||||
new Date(c.getLong(c.getColumnIndex("date"))),
|
||||
new String(c.getString(c.getColumnIndex ("text"))))
|
||||
messages += m
|
||||
}
|
||||
c.close()
|
||||
messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given new message into the database.
|
||||
*/
|
||||
def addMessage(message: Message): Unit = message match {
|
||||
case msg: TextMessage =>
|
||||
val cv: ContentValues = new ContentValues()
|
||||
cv.put("sender", msg.sender.toString)
|
||||
cv.put("receiver", msg.receiver.toString)
|
||||
// toString used as workaround for compile error with Long.
|
||||
cv.put("date", msg.date.getTime.toString)
|
||||
cv.put("text", msg.text)
|
||||
getWritableDatabase.insert("messages", null, cv)
|
||||
case msg: DeviceInfoMessage => // Never stored.
|
||||
}
|
||||
|
||||
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.nutomic.ensichat.messages
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.activities.AddContactsActivity
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.messages.Message._
|
||||
import org.msgpack.packer.Packer
|
||||
import org.msgpack.unpacker.Unpacker
|
||||
|
||||
object RequestAddContactMessage {
|
||||
|
||||
def read(sender: Device.ID, receiver: Device.ID, date: Date, up: Unpacker) =
|
||||
new RequestAddContactMessage(sender, receiver, date)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Message sent by [[AddContactsActivity]] to notify a device that it should be added as a contact.
|
||||
*/
|
||||
class RequestAddContactMessage(override val sender: Device.ID, override val receiver: Device.ID,
|
||||
override val date: Date) extends Message(Type.RequestAddContact) {
|
||||
|
||||
override def doWrite(packer: Packer) = {
|
||||
}
|
||||
|
||||
override def toString = "RequestAddContactMessage(" + sender.toString + ", " + receiver.toString +
|
||||
", " + date.toString + ")"
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.nutomic.ensichat.messages
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.activities.AddContactsActivity
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.messages.Message._
|
||||
import org.msgpack.packer.Packer
|
||||
import org.msgpack.unpacker.Unpacker
|
||||
|
||||
object ResultAddContactMessage {
|
||||
|
||||
def read(sender: Device.ID, receiver: Device.ID, date: Date, up: Unpacker) =
|
||||
new ResultAddContactMessage(sender, receiver, date, up.readBoolean())
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Message sent by [[AddContactsActivity]] to tell a device whether the user confirmed adding it
|
||||
* to contacts.
|
||||
*/
|
||||
class ResultAddContactMessage(override val sender: Device.ID, override val receiver: Device.ID,
|
||||
override val date: Date, val Accepted: Boolean)
|
||||
extends Message(Type.ResultAddContact) {
|
||||
|
||||
override def doWrite(packer: Packer) = packer.write(Accepted)
|
||||
|
||||
override def equals(a: Any) =
|
||||
super.equals(a) && a.asInstanceOf[ResultAddContactMessage].Accepted == Accepted
|
||||
|
||||
override def hashCode = super.hashCode + Accepted.hashCode
|
||||
|
||||
override def toString = "ResultAddContactMessage(" + sender.toString + ", " + receiver.toString +
|
||||
", " + date.toString + ", " + Accepted + ")"
|
||||
|
||||
}
|
127
app/src/main/scala/com/nutomic/ensichat/util/Database.scala
Normal file
127
app/src/main/scala/com/nutomic/ensichat/util/Database.scala
Normal file
|
@ -0,0 +1,127 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import android.content.{ContentValues, Context}
|
||||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.messages.{Message, TextMessage}
|
||||
|
||||
import scala.collection.SortedSet
|
||||
import scala.collection.immutable.TreeSet
|
||||
|
||||
object Database {
|
||||
|
||||
private val DatabaseName = "message_store.db"
|
||||
|
||||
private val DatabaseVersion = 1
|
||||
|
||||
private val CreateMessagesTable = "CREATE TABLE messages(" +
|
||||
"_id integer primary key autoincrement," +
|
||||
"sender string not null," +
|
||||
"receiver string not null," +
|
||||
"text blob not null," +
|
||||
"date integer not null);" // Unix timestamp of message.
|
||||
|
||||
private val CreateContactsTable = "CREATE TABLE contacts(" +
|
||||
"_id integer primary key autoincrement," +
|
||||
"device_id string not null," +
|
||||
"name string not null)"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all messages and contacts in SQL database.
|
||||
*/
|
||||
class Database(context: Context) extends SQLiteOpenHelper(context, Database.DatabaseName,
|
||||
null, Database.DatabaseVersion) {
|
||||
|
||||
private val Tag = "MessageStore"
|
||||
|
||||
private var contactsUpdatedListeners = Set[() => Unit]()
|
||||
|
||||
override def onCreate(db: SQLiteDatabase): Unit = {
|
||||
db.execSQL(Database.CreateContactsTable)
|
||||
db.execSQL(Database.CreateMessagesTable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count last messages for device.
|
||||
*/
|
||||
def getMessages(device: Device.ID, count: Int): SortedSet[Message] = {
|
||||
val c = getReadableDatabase.query(true,
|
||||
"messages", Array("sender", "receiver", "text", "date"),
|
||||
"sender = ? OR receiver = ?", Array(device.toString, device.toString),
|
||||
null, null, "date DESC", count.toString)
|
||||
var messages = new TreeSet[Message]()(Message.Ordering)
|
||||
while (c.moveToNext()) {
|
||||
val m = new TextMessage(
|
||||
new Device.ID(c.getString(c.getColumnIndex("sender"))),
|
||||
new Device.ID(c.getString(c.getColumnIndex("receiver"))),
|
||||
new Date(c.getLong(c.getColumnIndex("date"))),
|
||||
new String(c.getString(c.getColumnIndex ("text"))))
|
||||
messages += m
|
||||
}
|
||||
c.close()
|
||||
messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given new message into the database.
|
||||
*/
|
||||
def addMessage(message: Message): Unit = message match {
|
||||
case msg: TextMessage =>
|
||||
val cv = new ContentValues()
|
||||
cv.put("sender", msg.sender.toString)
|
||||
cv.put("receiver", msg.receiver.toString)
|
||||
// toString used as workaround for compile error with Long.
|
||||
cv.put("date", msg.date.getTime.toString)
|
||||
cv.put("text", msg.text)
|
||||
getWritableDatabase.insert("messages", null, cv)
|
||||
case _ => // Never stored.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all contacts of this device.
|
||||
*/
|
||||
def getContacts: Set[Device] = {
|
||||
val c = getReadableDatabase.query(true, "contacts", Array("device_id", "name"), "", Array(),
|
||||
null, null, "name DESC", null)
|
||||
var contacts = Set[Device]()
|
||||
while (c.moveToNext()) {
|
||||
contacts += new Device(new Device.ID(c.getString(c.getColumnIndex("device_id"))),
|
||||
c.getString(c.getColumnIndex("name")), false)
|
||||
}
|
||||
c.close()
|
||||
contacts
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a contact with the given device ID exists.
|
||||
*/
|
||||
def isContact(device: Device.ID): Boolean = {
|
||||
val c = getReadableDatabase.query(true, "contacts", Array("_id"), "device_id = ?",
|
||||
Array(device.toString), null, null, null, null)
|
||||
c.getCount != 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given device into contacts.
|
||||
*/
|
||||
def addContact(device: Device): Unit = {
|
||||
val cv = new ContentValues()
|
||||
cv.put("device_id", device.Id.toString)
|
||||
cv.put("name", device.Name)
|
||||
getWritableDatabase.insert("contacts", null, cv)
|
||||
contactsUpdatedListeners.foreach(_())
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a callback that is called whenever a new contact is added.
|
||||
*/
|
||||
def runOnContactsUpdated(l: () => Unit): Unit = contactsUpdatedListeners += l
|
||||
|
||||
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
|
||||
}
|
||||
|
||||
}
|
|
@ -11,11 +11,11 @@ import com.nutomic.ensichat.bluetooth.Device
|
|||
class DevicesAdapter(context: Context) extends
|
||||
ArrayAdapter[Device](context, android.R.layout.simple_list_item_1) {
|
||||
|
||||
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||
title.setText(getItem(position).name)
|
||||
view
|
||||
}
|
||||
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||
title.setText(getItem(position).Name)
|
||||
view
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Reference in a new issue