Added text chat functionality.
- ChatFragment with layout, Fragment handling in MainActivity - [MessagePack](http://msgpack.org/) serialization in TextMessage - MessageStore to hold old messages - various small code enhancements Way too many changes for one commit, but it's too late to change.
This commit is contained in:
parent
308d92185e
commit
3bd4feacbd
21 changed files with 572 additions and 162 deletions
|
@ -12,6 +12,7 @@ buildscript {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile "org.scala-lang:scala-library:2.11.2"
|
compile "org.scala-lang:scala-library:2.11.2"
|
||||||
|
compile "org.msgpack:msgpack-scala_2.11:0.6.11"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -42,14 +43,13 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
runProguard true
|
|
||||||
proguardFile file("proguard-rules.pro")
|
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
release {
|
// Avoid duplicate file errors during packaging.
|
||||||
runProguard true
|
packagingOptions {
|
||||||
proguardFile file("proguard-rules.pro")
|
exclude 'decoder.properties'
|
||||||
}
|
exclude 'rootdoc.txt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
app/proguard-rules.pro
vendored
24
app/proguard-rules.pro
vendored
|
@ -1,24 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /home/felix/software/android-studio/sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
-dontoptimize
|
|
||||||
-dontobfuscate
|
|
||||||
-dontpreverify
|
|
||||||
-dontwarn scala.**
|
|
||||||
-keep class !scala*.** { *; }
|
|
||||||
-ignorewarnings
|
|
BIN
app/src/main/res/drawable-hdpi/ic_action_send_now.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_action_send_now.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 465 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_send_now.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_action_send_now.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 332 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_send_now.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_action_send_now.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 578 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_send_now.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_action_send_now.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 759 B |
59
app/src/main/res/layout/fragment_chat.xml
Normal file
59
app/src/main/res/layout/fragment_chat.xml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="10">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_weight="11"
|
||||||
|
android:background="@android:color/background_light">
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@android:id/list"
|
||||||
|
android:drawSelectorOnTop="false"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:footerDividersEnabled="true" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:background="@android:color/darker_gray"
|
||||||
|
android:id="@+id/linearLayout">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/message"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:imeOptions="actionDone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/send"
|
||||||
|
android:layout_width="45dp"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:background="@drawable/ic_action_send_now"
|
||||||
|
android:onClick="sendMessage"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -7,15 +7,24 @@ import android.os.Bundle
|
||||||
import android.view.{Menu, MenuItem}
|
import android.view.{Menu, MenuItem}
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.bluetooth.ChatService
|
import com.nutomic.ensichat.bluetooth.{ChatService, Device}
|
||||||
|
import com.nutomic.ensichat.fragments.{ChatFragment, ContactsFragment}
|
||||||
|
|
||||||
|
import scala.collection.mutable.HashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main activity, holds fragments and requests Bluetooth to be discoverable.
|
* Main activity, entry point for app start.
|
||||||
*/
|
*/
|
||||||
class MainActivity extends Activity {
|
class MainActivity extends Activity {
|
||||||
|
|
||||||
private val RequestSetDiscoverable = 1
|
private val RequestSetDiscoverable = 1
|
||||||
|
|
||||||
|
private var ContactsFragment: ContactsFragment = _
|
||||||
|
|
||||||
|
private val ChatFragments = new HashMap[Device.ID, ChatFragment]()
|
||||||
|
|
||||||
|
private var currentChat: Option[Device.ID] = None
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes layout, starts service and requests Bluetooth to be discoverable.
|
* Initializes layout, starts service and requests Bluetooth to be discoverable.
|
||||||
*/
|
*/
|
||||||
|
@ -28,14 +37,43 @@ class MainActivity extends Activity {
|
||||||
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
|
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
|
||||||
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
|
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
|
||||||
startActivityForResult(intent, RequestSetDiscoverable)
|
startActivityForResult(intent, RequestSetDiscoverable)
|
||||||
|
val fm = getFragmentManager
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
ContactsFragment = fm.getFragment(savedInstanceState, classOf[ContactsFragment].getName)
|
||||||
|
.asInstanceOf[ContactsFragment]
|
||||||
|
for (i <- 0 until savedInstanceState.getInt("chat_fragments_count", 0)) {
|
||||||
|
val key = classOf[ChatFragment].getName + i
|
||||||
|
val cf = fm.getFragment(savedInstanceState, key).asInstanceOf[ChatFragment]
|
||||||
|
ChatFragments += (cf.getDevice -> cf)
|
||||||
|
}
|
||||||
|
currentChat = Some(new Device.ID(savedInstanceState.getString("current_chat")))
|
||||||
|
currentChat.collect{case c => openChat(c) }
|
||||||
|
} else {
|
||||||
|
ContactsFragment = new ContactsFragment()
|
||||||
|
}
|
||||||
|
fm.beginTransaction()
|
||||||
|
.add(android.R.id.content, ContactsFragment)
|
||||||
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes menu.
|
* Saves all fragment state.
|
||||||
*/
|
*/
|
||||||
|
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
getFragmentManager.putFragment(outState, classOf[ContactsFragment].getName, ContactsFragment)
|
||||||
|
outState.putInt("chat_fragments_count", ChatFragments.size)
|
||||||
|
var i: Int = 0
|
||||||
|
ChatFragments.foreach(cf => {
|
||||||
|
getFragmentManager.putFragment(outState, classOf[ChatFragment].getName + i, cf._2)
|
||||||
|
i += 1
|
||||||
|
})
|
||||||
|
outState.putString("current_chat", currentChat.toString)
|
||||||
|
}
|
||||||
|
|
||||||
override def onCreateOptionsMenu(menu: Menu): Boolean = {
|
override def onCreateOptionsMenu(menu: Menu): Boolean = {
|
||||||
getMenuInflater().inflate(R.menu.main, menu)
|
getMenuInflater.inflate(R.menu.main, menu)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,18 +89,45 @@ class MainActivity extends Activity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Menu click handler.
|
|
||||||
*/
|
|
||||||
override def onOptionsItemSelected(item: MenuItem): Boolean = {
|
override def onOptionsItemSelected(item: MenuItem): Boolean = {
|
||||||
item.getItemId match {
|
item.getItemId match {
|
||||||
case R.id.exit =>
|
case R.id.exit =>
|
||||||
stopService(new Intent(this, classOf[ChatService]))
|
stopService(new Intent(this, classOf[ChatService]))
|
||||||
finish()
|
finish()
|
||||||
return true
|
true
|
||||||
case _ =>
|
case _ =>
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a chat fragment for the given device, creating the fragment if needed.
|
||||||
|
*/
|
||||||
|
def openChat(device: Device.ID): Unit = {
|
||||||
|
currentChat = Some(device)
|
||||||
|
val ft = getFragmentManager.beginTransaction()
|
||||||
|
if (!ChatFragments.keySet.contains(device)) {
|
||||||
|
ChatFragments += (device -> new ChatFragment(device))
|
||||||
|
ft.add(android.R.id.content, ChatFragments.apply(device))
|
||||||
|
}
|
||||||
|
ft.detach(ContactsFragment)
|
||||||
|
.attach(ChatFragments.apply(device))
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If in a ChatFragment, goes back up to ContactsFragment.
|
||||||
|
*/
|
||||||
|
override def onBackPressed(): Unit = {
|
||||||
|
if (currentChat != None) {
|
||||||
|
getFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.detach(ChatFragments.apply(currentChat.get))
|
||||||
|
.attach(ContactsFragment)
|
||||||
|
.commit()
|
||||||
|
currentChat = None
|
||||||
|
} else
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,14 @@ import java.util.UUID
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
|
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
|
||||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||||
import android.os.{Handler, IBinder}
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.bluetooth.Device.ID
|
import com.nutomic.ensichat.bluetooth.ChatService.{OnDeviceConnectedListener, OnMessageReceivedListener}
|
||||||
import com.nutomic.ensichat.{Message, R}
|
import com.nutomic.ensichat.messages.{MessageStore, TextMessage}
|
||||||
|
|
||||||
import scala.collection.immutable.{HashMap, Set}
|
import scala.collection.immutable.{HashMap, HashSet, TreeSet}
|
||||||
|
import scala.collection.{SortedSet, mutable}
|
||||||
import scala.ref.WeakReference
|
import scala.ref.WeakReference
|
||||||
|
|
||||||
object ChatService {
|
object ChatService {
|
||||||
|
@ -21,31 +22,51 @@ object ChatService {
|
||||||
*/
|
*/
|
||||||
val appUuid: UUID = UUID.fromString("8ed52b7a-4501-5348-b054-3d94d004656e")
|
val appUuid: UUID = UUID.fromString("8ed52b7a-4501-5348-b054-3d94d004656e")
|
||||||
|
|
||||||
|
trait OnDeviceConnectedListener {
|
||||||
|
def onDeviceConnected(devices: Map[Device.ID, Device]): Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
trait OnMessageReceivedListener {
|
||||||
|
def onMessageReceived(messages: SortedSet[TextMessage]): Unit
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all Bluetooth connectivity.
|
||||||
|
*/
|
||||||
class ChatService extends Service {
|
class ChatService extends Service {
|
||||||
|
|
||||||
private val Tag = "ChatService"
|
private val Tag = "ChatService"
|
||||||
|
|
||||||
private val SCAN_INTERVAL: Int = 5000
|
private val ScanInterval = 5000
|
||||||
|
|
||||||
private final val Binder = new ChatServiceBinder(this)
|
private val Binder = new ChatServiceBinder(this)
|
||||||
|
|
||||||
private var bluetoothAdapter: BluetoothAdapter = _
|
private var bluetoothAdapter: BluetoothAdapter = _
|
||||||
|
|
||||||
private var deviceListener: Set[WeakReference[Map[Device.ID, Device] => Unit]] =
|
/**
|
||||||
Set[WeakReference[Map[Device.ID, Device] => Unit]]()
|
* 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
|
||||||
|
* referenced.
|
||||||
|
*/
|
||||||
|
private var deviceListeners = new HashSet[WeakReference[OnDeviceConnectedListener]]()
|
||||||
|
|
||||||
private var devices: HashMap[Device.ID, Device] = new HashMap[Device.ID, Device]()
|
private val messageListeners =
|
||||||
|
mutable.HashMap[Device.ID, mutable.Set[WeakReference[OnMessageReceivedListener]]]()
|
||||||
|
.withDefaultValue(mutable.Set[WeakReference[OnMessageReceivedListener]]())
|
||||||
|
|
||||||
private var connections: HashMap[Device.ID, TransferThread] =
|
private var devices = new HashMap[Device.ID, Device]()
|
||||||
new HashMap[Device.ID, TransferThread]()
|
|
||||||
|
private var connections = new HashMap[Device.ID, TransferThread]()
|
||||||
|
|
||||||
private var ListenThread: ListenThread = _
|
private var ListenThread: ListenThread = _
|
||||||
|
|
||||||
private var cancelDiscovery = false
|
private var cancelDiscovery = false
|
||||||
|
|
||||||
private val MainHandler: Handler = new Handler()
|
private val MainHandler = new Handler()
|
||||||
|
|
||||||
|
private var MessageStore: MessageStore = _
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes BroadcastReceiver for discovery, starts discovery and listens for connections.
|
* Initializes BroadcastReceiver for discovery, starts discovery and listens for connections.
|
||||||
|
@ -53,7 +74,9 @@ class ChatService extends Service {
|
||||||
override def onCreate(): Unit = {
|
override def onCreate(): Unit = {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
MessageStore = new MessageStore(this)
|
||||||
|
|
||||||
|
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter
|
||||||
|
|
||||||
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
|
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
|
||||||
registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
|
||||||
|
@ -62,14 +85,13 @@ class ChatService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
|
override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
|
||||||
return Service.START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override def onBind(intent: Intent): IBinder = {
|
override def onBind(intent: Intent) = Binder
|
||||||
return Binder
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops discovery, listening and unregisters receivers.
|
||||||
|
*/
|
||||||
override def onDestroy(): Unit = {
|
override def onDestroy(): Unit = {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
ListenThread.cancel()
|
ListenThread.cancel()
|
||||||
|
@ -85,14 +107,14 @@ class ChatService extends Service {
|
||||||
if (cancelDiscovery)
|
if (cancelDiscovery)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (!bluetoothAdapter.isDiscovering()) {
|
if (!bluetoothAdapter.isDiscovering) {
|
||||||
Log.v(Tag, "Running discovery")
|
Log.v(Tag, "Running discovery")
|
||||||
bluetoothAdapter.startDiscovery()
|
bluetoothAdapter.startDiscovery()
|
||||||
}
|
}
|
||||||
|
|
||||||
MainHandler.postDelayed(new Runnable {
|
MainHandler.postDelayed(new Runnable {
|
||||||
override def run(): Unit = discover()
|
override def run(): Unit = discover()
|
||||||
}, SCAN_INTERVAL)
|
}, ScanInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,7 +124,7 @@ class ChatService extends Service {
|
||||||
override def onReceive(context: Context, intent: Intent) {
|
override def onReceive(context: Context, intent: Intent) {
|
||||||
val device: Device =
|
val device: Device =
|
||||||
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
|
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
|
||||||
devices = devices + (device.id -> device)
|
devices += (device.id -> device)
|
||||||
new ConnectThread(device, onConnected).start()
|
new ConnectThread(device, onConnected).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +140,7 @@ class ChatService extends Service {
|
||||||
case BluetoothAdapter.STATE_TURNING_OFF =>
|
case BluetoothAdapter.STATE_TURNING_OFF =>
|
||||||
connections.foreach(d => d._2.close())
|
connections.foreach(d => d._2.close())
|
||||||
case BluetoothAdapter.STATE_OFF =>
|
case BluetoothAdapter.STATE_OFF =>
|
||||||
Log.d(Tag, "Bluetooth disabled, stopping listening and discovery")
|
Log.i(Tag, "Bluetooth disabled, stopping listening and discovery")
|
||||||
if (ListenThread != null) {
|
if (ListenThread != null) {
|
||||||
ListenThread.cancel()
|
ListenThread.cancel()
|
||||||
}
|
}
|
||||||
|
@ -132,7 +154,6 @@ class ChatService extends Service {
|
||||||
* Starts to listen for incoming connections, and starts regular active discovery.
|
* Starts to listen for incoming connections, and starts regular active discovery.
|
||||||
*/
|
*/
|
||||||
private def startBluetoothConnections(): Unit = {
|
private def startBluetoothConnections(): Unit = {
|
||||||
Log.i(Tag, "Listening and discovery started")
|
|
||||||
cancelDiscovery = false
|
cancelDiscovery = false
|
||||||
discover()
|
discover()
|
||||||
ListenThread = new ListenThread(getString(R.string.app_name), bluetoothAdapter, onConnected)
|
ListenThread = new ListenThread(getString(R.string.app_name), bluetoothAdapter, onConnected)
|
||||||
|
@ -142,42 +163,64 @@ class ChatService extends Service {
|
||||||
/**
|
/**
|
||||||
* Registers a listener that is called whenever a new device is connected.
|
* Registers a listener that is called whenever a new device is connected.
|
||||||
*/
|
*/
|
||||||
def registerDeviceListener(listener: Map[Device.ID, Device] => Unit): Unit = {
|
def registerDeviceListener(listener: OnDeviceConnectedListener): Unit = {
|
||||||
deviceListener = deviceListener + new WeakReference[(Map[ID, Device]) => Unit](listener)
|
deviceListeners += new WeakReference[OnDeviceConnectedListener](listener)
|
||||||
listener(devices)
|
listener.onDeviceConnected(devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters a device listener.
|
* Called when a Bluetooth device is connected.
|
||||||
|
*
|
||||||
|
* Adds the device to [[connections]], notifies all [[deviceListeners]].
|
||||||
*/
|
*/
|
||||||
def unregisterDeviceListener(listener: Map[Device.ID, Device] => Unit): Unit = {
|
|
||||||
deviceListener.foreach(l =>
|
|
||||||
if (l == listener)
|
|
||||||
deviceListener = deviceListener - l)
|
|
||||||
}
|
|
||||||
|
|
||||||
def onConnected(device: Device, socket: BluetoothSocket): Unit = {
|
def onConnected(device: Device, socket: BluetoothSocket): Unit = {
|
||||||
val updatedDevice: Device = new Device(device.bluetoothDevice, true)
|
val updatedDevice: Device = new Device(device.bluetoothDevice, true)
|
||||||
devices = devices + (device.id -> updatedDevice)
|
devices += (device.id -> updatedDevice)
|
||||||
connections = connections + (device.id -> new TransferThread(updatedDevice, socket, onReceive))
|
connections += (device.id -> new TransferThread(updatedDevice, socket, handleNewMessage))
|
||||||
connections(device.id).start()
|
connections(device.id).start()
|
||||||
deviceListener.foreach(d =>
|
deviceListeners.foreach(l => l.get match {
|
||||||
if (d != null)
|
case Some(_) => l.apply().onDeviceConnected(devices)
|
||||||
d.apply()(devices)
|
case None => deviceListeners -= l
|
||||||
else
|
|
||||||
deviceListener = deviceListener - d)
|
|
||||||
}
|
|
||||||
|
|
||||||
def send(device: Device.ID, message: Message): Unit = {
|
|
||||||
connections.apply(device).send(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
def onReceive(device: Device.ID, message: Message): Unit = {
|
|
||||||
MainHandler.post(new Runnable {
|
|
||||||
override def run(): Unit =
|
|
||||||
Toast.makeText(ChatService.this, devices(device).name + " sent: " + message, Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends message to the device specified as receiver,
|
||||||
|
*/
|
||||||
|
def send(message: TextMessage): Unit = {
|
||||||
|
connections.apply(message.receiver).send(message)
|
||||||
|
handleNewMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the message to database and sends it to registered listeners.
|
||||||
|
*
|
||||||
|
* If you want to send a new message, use [[send]].
|
||||||
|
*/
|
||||||
|
def handleNewMessage(message: TextMessage): Unit = {
|
||||||
|
MessageStore.addMessage(message)
|
||||||
|
MainHandler.post(new Runnable {
|
||||||
|
override def run(): Unit = {
|
||||||
|
messageListeners(message.sender).foreach(l =>
|
||||||
|
if (l.get != null)
|
||||||
|
l.apply().onMessageReceived(new TreeSet[TextMessage]()(TextMessage.Ordering) + message)
|
||||||
|
else
|
||||||
|
messageListeners(message.sender) -= l)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the unique bluetooth address of the local device.
|
||||||
|
*/
|
||||||
|
def localDeviceId = new Device.ID(bluetoothAdapter.getAddress)
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,10 +2,8 @@ package com.nutomic.ensichat.bluetooth
|
||||||
|
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
|
|
||||||
class ChatServiceBinder (mService: ChatService) extends Binder {
|
class ChatServiceBinder (service: ChatService) extends Binder {
|
||||||
|
|
||||||
def getService(): ChatService = {
|
def getService = service
|
||||||
return mService
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
|
||||||
device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid)
|
device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid)
|
||||||
|
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
|
Log.i(Tag, "Connecting to " + device.toString)
|
||||||
try {
|
try {
|
||||||
socket.connect()
|
socket.connect()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -25,7 +26,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
|
||||||
socket.close()
|
socket.close()
|
||||||
} catch {
|
} catch {
|
||||||
case e2: IOException =>
|
case e2: IOException =>
|
||||||
Log.e(Tag, "Failed to close socket", e2);
|
Log.e(Tag, "Failed to close socket", e2)
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.nutomic.ensichat.bluetooth
|
package com.nutomic.ensichat.bluetooth
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import org.msgpack.annotation.Message
|
||||||
|
|
||||||
object Device {
|
object Device {
|
||||||
|
|
||||||
|
@ -9,12 +10,14 @@ object Device {
|
||||||
*
|
*
|
||||||
* @param Id A bluetooth device address.
|
* @param Id A bluetooth device address.
|
||||||
*/
|
*/
|
||||||
|
@Message
|
||||||
class ID(private val Id: String) {
|
class ID(private val Id: String) {
|
||||||
override def hashCode = Id.hashCode
|
override def hashCode = Id.hashCode
|
||||||
override def equals(a: Any) = a match {
|
override def equals(a: Any) = a match {
|
||||||
case other: Device.ID => Id == other.Id
|
case other: Device.ID => Id == other.Id
|
||||||
case _ => false
|
case _ => false
|
||||||
}
|
}
|
||||||
|
override def toString = Id
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -32,4 +35,6 @@ class Device(BluetoothDevice: BluetoothDevice, Connected: Boolean) {
|
||||||
|
|
||||||
def bluetoothDevice = BluetoothDevice
|
def bluetoothDevice = BluetoothDevice
|
||||||
|
|
||||||
|
override def toString = "Device(" + name + ", " + bluetoothDevice.getAddress + ")"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
|
||||||
}
|
}
|
||||||
|
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
|
Log.i(Tag, "Listening for connections")
|
||||||
var socket: BluetoothSocket = null
|
var socket: BluetoothSocket = null
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package com.nutomic.ensichat.bluetooth
|
package com.nutomic.ensichat.bluetooth
|
||||||
|
|
||||||
import java.io.{OutputStream, InputStream}
|
import java.io.{IOException, InputStream, OutputStream}
|
||||||
|
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.nutomic.ensichat.Message
|
import com.nutomic.ensichat.messages.TextMessage
|
||||||
import java.io.IOException
|
|
||||||
/**
|
/**
|
||||||
* Transfers data between connnected devices.
|
* Transfers data between connnected devices.
|
||||||
*
|
*
|
||||||
|
@ -14,13 +14,13 @@ import java.io.IOException
|
||||||
* @param onReceive Called when a message was received from the other device.
|
* @param onReceive Called when a message was received from the other device.
|
||||||
*/
|
*/
|
||||||
class TransferThread(device: Device, socket: BluetoothSocket,
|
class TransferThread(device: Device, socket: BluetoothSocket,
|
||||||
onReceive: (Device.ID, Message) => Unit) extends Thread {
|
onReceive: (TextMessage) => Unit) extends Thread {
|
||||||
|
|
||||||
val Tag: String = "TransferThread"
|
val Tag: String = "TransferThread"
|
||||||
|
|
||||||
val InStream: InputStream =
|
val InStream: InputStream =
|
||||||
try {
|
try {
|
||||||
socket.getInputStream()
|
socket.getInputStream
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.e(Tag, "Failed to open stream", e)
|
Log.e(Tag, "Failed to open stream", e)
|
||||||
|
@ -29,7 +29,7 @@ class TransferThread(device: Device, socket: BluetoothSocket,
|
||||||
|
|
||||||
val OutStream: OutputStream =
|
val OutStream: OutputStream =
|
||||||
try {
|
try {
|
||||||
socket.getOutputStream()
|
socket.getOutputStream
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.e(Tag, "Failed to open stream", e)
|
Log.e(Tag, "Failed to open stream", e)
|
||||||
|
@ -37,28 +37,26 @@ class TransferThread(device: Device, socket: BluetoothSocket,
|
||||||
}
|
}
|
||||||
|
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
var buffer: Array[Byte] = new Array(1024)
|
Log.i(Tag, "Starting data transfer with " + device.toString)
|
||||||
|
|
||||||
// Keep listening to the InputStream while connected
|
// Keep listening to the InputStream while connected
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
InStream.read(buffer)
|
val msg = TextMessage.fromStream(InStream)
|
||||||
val msg: Message = Message.fromByteArray(buffer)
|
onReceive(msg)
|
||||||
onReceive(device.id, msg)
|
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.e(Tag, "Disconnected from device", e);
|
Log.e(Tag, "Disconnected from device", e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def send(message: Message): Unit = {
|
def send(message: TextMessage): Unit = {
|
||||||
try {
|
try {
|
||||||
OutStream.write(message.toByteArray())
|
message.write(OutStream)
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.e(Tag, "Failed to write message", e);
|
Log.e(Tag, "Failed to write message", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
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.view.View.OnClickListener
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.{KeyEvent, LayoutInflater, View, ViewGroup}
|
||||||
|
import android.widget.TextView.OnEditorActionListener
|
||||||
|
import android.widget._
|
||||||
|
import com.nutomic.ensichat.R
|
||||||
|
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||||
|
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
|
||||||
|
import com.nutomic.ensichat.messages.TextMessage
|
||||||
|
import com.nutomic.ensichat.util.MessagesAdapter
|
||||||
|
|
||||||
|
import scala.collection.SortedSet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single chat with another specific device.
|
||||||
|
*/
|
||||||
|
class ChatFragment extends ListFragment with OnClickListener
|
||||||
|
with OnMessageReceivedListener {
|
||||||
|
|
||||||
|
def this(device: Device.ID) {
|
||||||
|
this
|
||||||
|
this.device = device
|
||||||
|
}
|
||||||
|
|
||||||
|
private var device: Device.ID = _
|
||||||
|
|
||||||
|
private var chatService: ChatService = _
|
||||||
|
|
||||||
|
private var sendButton: Button = _
|
||||||
|
|
||||||
|
private var messageText: EditText = _
|
||||||
|
|
||||||
|
private var listView: ListView = _
|
||||||
|
|
||||||
|
private var adapter: ArrayAdapter[TextMessage] = _
|
||||||
|
|
||||||
|
private final val mChatServiceConnection: ServiceConnection = new ServiceConnection {
|
||||||
|
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
|
||||||
|
val binder: ChatServiceBinder = iBinder.asInstanceOf[ChatServiceBinder]
|
||||||
|
chatService = binder.getService
|
||||||
|
// Read local device ID from service,
|
||||||
|
adapter = new MessagesAdapter(getActivity, chatService.localDeviceId)
|
||||||
|
chatService.registerMessageListener(device, ChatFragment.this)
|
||||||
|
if (listView != null) {
|
||||||
|
listView.setAdapter(adapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onServiceDisconnected(componentName: ComponentName): Unit = {
|
||||||
|
chatService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||||
|
savedInstanceState: Bundle): View = {
|
||||||
|
val view: View = inflater.inflate(R.layout.fragment_chat, container, false)
|
||||||
|
sendButton = view.findViewById(R.id.send).asInstanceOf[Button]
|
||||||
|
sendButton.setOnClickListener(this)
|
||||||
|
messageText = view.findViewById(R.id.message).asInstanceOf[EditText]
|
||||||
|
messageText.setOnEditorActionListener(new OnEditorActionListener {
|
||||||
|
override def onEditorAction(view: TextView, actionId: Int, event: KeyEvent): Boolean = {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
onClick(sendButton)
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
listView = view.findViewById(android.R.id.list).asInstanceOf[ListView]
|
||||||
|
listView.setAdapter(adapter)
|
||||||
|
view
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
getActivity.bindService(new Intent(getActivity, classOf[ChatService]),
|
||||||
|
mChatServiceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
device = new Device.ID(savedInstanceState.getString("device"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putString("device", device.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onDestroy(): Unit = {
|
||||||
|
super.onDestroy()
|
||||||
|
getActivity.unbindService(mChatServiceConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message if send button was clicked.
|
||||||
|
*/
|
||||||
|
override def onClick(view: View): Unit = {
|
||||||
|
view.getId match {
|
||||||
|
case R.id.send =>
|
||||||
|
val text: String = messageText.getText.toString
|
||||||
|
if (!text.isEmpty) {
|
||||||
|
chatService.send(
|
||||||
|
new TextMessage(chatService.localDeviceId, device, text.toString, new Date()))
|
||||||
|
messageText.getText.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays new messages in UI.
|
||||||
|
*/
|
||||||
|
override def onMessageReceived(messages: SortedSet[TextMessage]): Unit = {
|
||||||
|
messages.foreach(m => adapter.add(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the device that this fragment shows chats for.
|
||||||
|
*/
|
||||||
|
def getDevice = this.device
|
||||||
|
|
||||||
|
}
|
|
@ -3,22 +3,25 @@ package com.nutomic.ensichat.fragments
|
||||||
import android.app.ListFragment
|
import android.app.ListFragment
|
||||||
import android.content.{ComponentName, Context, Intent, ServiceConnection}
|
import android.content.{ComponentName, Context, Intent, ServiceConnection}
|
||||||
import android.os.{Bundle, IBinder}
|
import android.os.{Bundle, IBinder}
|
||||||
import android.util.Log
|
|
||||||
import android.view.{LayoutInflater, View, ViewGroup}
|
import android.view.{LayoutInflater, View, ViewGroup}
|
||||||
import android.widget.{ArrayAdapter, ListView}
|
import android.widget.{ArrayAdapter, ListView}
|
||||||
|
import com.nutomic.ensichat.R
|
||||||
|
import com.nutomic.ensichat.activities.MainActivity
|
||||||
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
|
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
|
||||||
import com.nutomic.ensichat.util.DevicesAdapter
|
import com.nutomic.ensichat.util.DevicesAdapter
|
||||||
import com.nutomic.ensichat.{Message, R}
|
|
||||||
|
|
||||||
class ContactsFragment extends ListFragment {
|
/**
|
||||||
|
* Lists all nearby, connected devices.
|
||||||
|
*/
|
||||||
|
class ContactsFragment extends ListFragment with ChatService.OnDeviceConnectedListener {
|
||||||
|
|
||||||
private var chatService: ChatService = _
|
private var chatService: ChatService = _
|
||||||
|
|
||||||
private final val mChatServiceConnection: ServiceConnection = new ServiceConnection {
|
private final val ChatServiceConnection: ServiceConnection = new ServiceConnection {
|
||||||
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
|
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
|
||||||
val binder: ChatServiceBinder = iBinder.asInstanceOf[ChatServiceBinder]
|
val binder: ChatServiceBinder = iBinder.asInstanceOf[ChatServiceBinder]
|
||||||
chatService = binder.getService()
|
chatService = binder.getService
|
||||||
chatService.registerDeviceListener(onDeviceConnected)
|
chatService.registerDeviceListener(ContactsFragment.this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onServiceDisconnected(componentName: ComponentName): Unit = {
|
override def onServiceDisconnected(componentName: ComponentName): Unit = {
|
||||||
|
@ -31,7 +34,7 @@ class ContactsFragment extends ListFragment {
|
||||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||||
savedInstanceState: Bundle): View = {
|
savedInstanceState: Bundle): View = {
|
||||||
val view: View = inflater.inflate(R.layout.fragment_contacts, container, false)
|
val view: View = inflater.inflate(R.layout.fragment_contacts, container, false)
|
||||||
return view
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||||
|
@ -40,19 +43,21 @@ class ContactsFragment extends ListFragment {
|
||||||
adapter = new DevicesAdapter(getActivity)
|
adapter = new DevicesAdapter(getActivity)
|
||||||
setListAdapter(adapter)
|
setListAdapter(adapter)
|
||||||
getActivity.bindService(new Intent(getActivity, classOf[ChatService]),
|
getActivity.bindService(new Intent(getActivity, classOf[ChatService]),
|
||||||
mChatServiceConnection, Context.BIND_AUTO_CREATE)
|
ChatServiceConnection, Context.BIND_AUTO_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onDestroy(): Unit = {
|
override def onDestroy(): Unit = {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
getActivity.unbindService(mChatServiceConnection)
|
getActivity.unbindService(ChatServiceConnection)
|
||||||
chatService.unregisterDeviceListener(onDeviceConnected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays all connected devices in the listview.
|
* Displays newly connected devices in the list.
|
||||||
*/
|
*/
|
||||||
def onDeviceConnected(devices: Map[Device.ID, Device]): Unit = {
|
override def onDeviceConnected(devices: Map[Device.ID, Device]): Unit = {
|
||||||
|
if (getActivity == null)
|
||||||
|
return
|
||||||
|
|
||||||
val filtered = devices.filter{ case (_, d) => d.connected }
|
val filtered = devices.filter{ case (_, d) => d.connected }
|
||||||
getActivity.runOnUiThread(new Runnable {
|
getActivity.runOnUiThread(new Runnable {
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
|
@ -63,10 +68,10 @@ class ContactsFragment extends ListFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a ping message to the clicked device.
|
* Opens a chat with the clicked device.
|
||||||
*/
|
*/
|
||||||
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit = {
|
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit = {
|
||||||
chatService.send(adapter.getItem(position).id, new Message("Ping"))
|
getActivity.asInstanceOf[MainActivity].openChat(adapter.getItem(position).id)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package com.nutomic.ensichat
|
|
||||||
|
|
||||||
object Message {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new message from transferred bytes.
|
|
||||||
*/
|
|
||||||
def fromByteArray(data: Array[Byte]): Message = {
|
|
||||||
return new Message(new String(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for all messages that can be passed between bluetooth devices.
|
|
||||||
*
|
|
||||||
* Provides methods for (de-)serialization.
|
|
||||||
*/
|
|
||||||
class Message(text: String) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts message to bytes for transfer.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
def toByteArray(): Array[Byte] = {
|
|
||||||
return text.getBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
override def toString = text
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
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[TextMessage] = {
|
||||||
|
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[TextMessage] = new TreeSet[TextMessage]()(TextMessage.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 String(c.getBlob(c.getColumnIndex ("text"))),
|
||||||
|
new Date(c.getLong(c.getColumnIndex("date"))))
|
||||||
|
messages += m
|
||||||
|
}
|
||||||
|
c.close()
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts the given new message into the database.
|
||||||
|
*/
|
||||||
|
def addMessage(message: TextMessage): Unit = {
|
||||||
|
val cv: ContentValues = new ContentValues()
|
||||||
|
cv.put("sender", message.sender.toString)
|
||||||
|
cv.put("receiver", message.receiver.toString)
|
||||||
|
cv.put("text", message.text)
|
||||||
|
cv.put("date", message.date.getTime.toString) // toString used as workaround for compile error
|
||||||
|
getWritableDatabase.insert("messages", null, cv)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.nutomic.ensichat.messages
|
||||||
|
|
||||||
|
import java.io.{InputStream, OutputStream}
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
import com.nutomic.ensichat.bluetooth.Device
|
||||||
|
import org.msgpack.ScalaMessagePack
|
||||||
|
|
||||||
|
object TextMessage {
|
||||||
|
|
||||||
|
val Ordering = new Ordering[TextMessage] {
|
||||||
|
override def compare(m1: TextMessage, m2: TextMessage) = m1.date.compareTo(m2.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new message from stream.
|
||||||
|
*/
|
||||||
|
def fromStream(in: InputStream): TextMessage = {
|
||||||
|
val up = new ScalaMessagePack().createUnpacker(in)
|
||||||
|
new TextMessage(
|
||||||
|
new Device.ID(up.read(classOf[String])),
|
||||||
|
new Device.ID(up.read(classOf[String])),
|
||||||
|
up.read(classOf[String]),
|
||||||
|
new Date(up.read(classOf[Long])))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents content and metadata that can be transferred between devices.
|
||||||
|
*/
|
||||||
|
class TextMessage(val sender: Device.ID, val receiver: Device.ID,
|
||||||
|
val text: String, val date: Date) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes this object into stream.
|
||||||
|
*/
|
||||||
|
def write(os: OutputStream): Unit = {
|
||||||
|
new ScalaMessagePack().createPacker(os)
|
||||||
|
.write(sender.toString)
|
||||||
|
.write(receiver.toString)
|
||||||
|
.write(text)
|
||||||
|
.write(date.getTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def equals(a: Any) = a match {
|
||||||
|
case o: TextMessage =>
|
||||||
|
sender == o.sender && receiver == o.receiver && text == o.text && date == o.date
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
|
||||||
|
override def hashCode() = sender.hashCode + receiver.hashCode + text.hashCode + date.hashCode()
|
||||||
|
|
||||||
|
override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString +
|
||||||
|
", " + text + ", " + date.toString + ")"
|
||||||
|
}
|
|
@ -1,17 +1,21 @@
|
||||||
package com.nutomic.ensichat.util
|
package com.nutomic.ensichat.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.{ViewGroup, View}
|
import android.view.{View, ViewGroup}
|
||||||
import android.widget.{TextView, ArrayAdapter}
|
import android.widget.{ArrayAdapter, TextView}
|
||||||
import com.nutomic.ensichat.bluetooth.Device
|
import com.nutomic.ensichat.bluetooth.Device
|
||||||
|
|
||||||
class DevicesAdapter(context: Context) extends ArrayAdapter[Device](context, android.R.layout.simple_list_item_1) {
|
/**
|
||||||
|
* Displays [[Device]]s in ListView.
|
||||||
|
*/
|
||||||
|
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 = {
|
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
||||||
val view = super.getView(position, convertView, parent)
|
val view = super.getView(position, convertView, parent)
|
||||||
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||||
title.setText(getItem(position).name)
|
title.setText(getItem(position).name)
|
||||||
return view
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.nutomic.ensichat.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.{View, ViewGroup}
|
||||||
|
import android.widget.{ArrayAdapter, TextView}
|
||||||
|
import com.nutomic.ensichat.bluetooth.Device
|
||||||
|
import com.nutomic.ensichat.messages.TextMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays [[TextMessage]]s in ListView.
|
||||||
|
*/
|
||||||
|
class MessagesAdapter(context: Context, localDevice: Device.ID) extends
|
||||||
|
ArrayAdapter[TextMessage](context, android.R.layout.simple_list_item_1) {
|
||||||
|
|
||||||
|
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
||||||
|
val view: View = super.getView(position, convertView, parent)
|
||||||
|
val tv: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||||
|
view.setBackgroundColor(context.getResources.getColor(
|
||||||
|
if (getItem(position).sender == localDevice) android.R.color.holo_blue_light
|
||||||
|
else android.R.color.holo_green_light))
|
||||||
|
tv.setText(getItem(position).text)
|
||||||
|
view
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in a new issue