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:
Felix Ableitner 2014-10-30 00:23:01 +02:00
parent 308d92185e
commit 3bd4feacbd
21 changed files with 572 additions and 162 deletions

View File

@ -12,6 +12,7 @@ buildscript {
dependencies {
compile "org.scala-lang:scala-library:2.11.2"
compile "org.msgpack:msgpack-scala_2.11:0.6.11"
}
android {
@ -42,14 +43,13 @@ android {
buildTypes {
debug {
runProguard true
proguardFile file("proguard-rules.pro")
applicationIdSuffix ".debug"
}
}
release {
runProguard true
proguardFile file("proguard-rules.pro")
}
// Avoid duplicate file errors during packaging.
packagingOptions {
exclude 'decoder.properties'
exclude 'rootdoc.txt'
}
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

View 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>

View File

@ -7,15 +7,24 @@ import android.os.Bundle
import android.view.{Menu, MenuItem}
import android.widget.Toast
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 {
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.
*/
@ -28,14 +37,43 @@ class MainActivity extends Activity {
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0)
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 = {
getMenuInflater().inflate(R.menu.main, menu)
return true
getMenuInflater.inflate(R.menu.main, menu)
true
}
/**
@ -51,18 +89,45 @@ class MainActivity extends Activity {
}
}
/**
* Menu click handler.
*/
override def onOptionsItemSelected(item: MenuItem): Boolean = {
item.getItemId match {
case R.id.exit =>
stopService(new Intent(this, classOf[ChatService]))
finish()
return true
true
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()
}
}

View File

@ -5,13 +5,14 @@ import java.util.UUID
import android.app.Service
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.{Handler, IBinder}
import android.os.Handler
import android.util.Log
import android.widget.Toast
import com.nutomic.ensichat.bluetooth.Device.ID
import com.nutomic.ensichat.{Message, R}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.bluetooth.ChatService.{OnDeviceConnectedListener, OnMessageReceivedListener}
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
object ChatService {
@ -21,31 +22,51 @@ object ChatService {
*/
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 {
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 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] =
new HashMap[Device.ID, TransferThread]()
private var devices = new HashMap[Device.ID, Device]()
private var connections = new HashMap[Device.ID, TransferThread]()
private var ListenThread: ListenThread = _
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.
@ -53,7 +74,9 @@ class ChatService extends Service {
override def onCreate(): Unit = {
super.onCreate()
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
MessageStore = new MessageStore(this)
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
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 = {
return Service.START_STICKY
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
override def onBind(intent: Intent): IBinder = {
return Binder
}
override def onBind(intent: Intent) = Binder
/**
* Stops discovery, listening and unregisters receivers.
*/
override def onDestroy(): Unit = {
super.onDestroy()
ListenThread.cancel()
@ -85,14 +107,14 @@ class ChatService extends Service {
if (cancelDiscovery)
return
if (!bluetoothAdapter.isDiscovering()) {
if (!bluetoothAdapter.isDiscovering) {
Log.v(Tag, "Running discovery")
bluetoothAdapter.startDiscovery()
}
MainHandler.postDelayed(new Runnable {
override def run(): Unit = discover()
}, SCAN_INTERVAL)
}, ScanInterval)
}
/**
@ -102,7 +124,7 @@ class ChatService extends Service {
override def onReceive(context: Context, intent: Intent) {
val device: Device =
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
devices = devices + (device.id -> device)
devices += (device.id -> device)
new ConnectThread(device, onConnected).start()
}
}
@ -118,7 +140,7 @@ class ChatService extends Service {
case BluetoothAdapter.STATE_TURNING_OFF =>
connections.foreach(d => d._2.close())
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) {
ListenThread.cancel()
}
@ -132,7 +154,6 @@ class ChatService extends Service {
* Starts to listen for incoming connections, and starts regular active discovery.
*/
private def startBluetoothConnections(): Unit = {
Log.i(Tag, "Listening and discovery started")
cancelDiscovery = false
discover()
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.
*/
def registerDeviceListener(listener: Map[Device.ID, Device] => Unit): Unit = {
deviceListener = deviceListener + new WeakReference[(Map[ID, Device]) => Unit](listener)
listener(devices)
def registerDeviceListener(listener: OnDeviceConnectedListener): Unit = {
deviceListeners += new WeakReference[OnDeviceConnectedListener](listener)
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 = {
val updatedDevice: Device = new Device(device.bluetoothDevice, true)
devices = devices + (device.id -> updatedDevice)
connections = connections + (device.id -> new TransferThread(updatedDevice, socket, onReceive))
devices += (device.id -> updatedDevice)
connections += (device.id -> new TransferThread(updatedDevice, socket, handleNewMessage))
connections(device.id).start()
deviceListener.foreach(d =>
if (d != null)
d.apply()(devices)
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()
deviceListeners.foreach(l => l.get match {
case Some(_) => l.apply().onDeviceConnected(devices)
case None => deviceListeners -= l
})
}
/**
* 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)
}

View File

@ -2,10 +2,8 @@ package com.nutomic.ensichat.bluetooth
import android.os.Binder
class ChatServiceBinder (mService: ChatService) extends Binder {
class ChatServiceBinder (service: ChatService) extends Binder {
def getService(): ChatService = {
return mService
}
def getService = service
}

View File

@ -17,6 +17,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid)
override def run(): Unit = {
Log.i(Tag, "Connecting to " + device.toString)
try {
socket.connect()
} catch {
@ -25,7 +26,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
socket.close()
} catch {
case e2: IOException =>
Log.e(Tag, "Failed to close socket", e2);
Log.e(Tag, "Failed to close socket", e2)
}
return;
}

View File

@ -1,6 +1,7 @@
package com.nutomic.ensichat.bluetooth
import android.bluetooth.BluetoothDevice
import org.msgpack.annotation.Message
object Device {
@ -9,12 +10,14 @@ object Device {
*
* @param Id A bluetooth device address.
*/
@Message
class ID(private val Id: String) {
override def hashCode = Id.hashCode
override def equals(a: Any) = a match {
case other: Device.ID => Id == other.Id
case _ => false
}
override def toString = Id
}
}
@ -32,4 +35,6 @@ class Device(BluetoothDevice: BluetoothDevice, Connected: Boolean) {
def bluetoothDevice = BluetoothDevice
override def toString = "Device(" + name + ", " + bluetoothDevice.getAddress + ")"
}

View File

@ -23,6 +23,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
}
override def run(): Unit = {
Log.i(Tag, "Listening for connections")
var socket: BluetoothSocket = null
while (true) {

View File

@ -1,11 +1,11 @@
package com.nutomic.ensichat.bluetooth
import java.io.{OutputStream, InputStream}
import java.io.{IOException, InputStream, OutputStream}
import android.bluetooth.BluetoothSocket
import android.util.Log
import com.nutomic.ensichat.Message
import java.io.IOException
import com.nutomic.ensichat.messages.TextMessage
/**
* 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.
*/
class TransferThread(device: Device, socket: BluetoothSocket,
onReceive: (Device.ID, Message) => Unit) extends Thread {
onReceive: (TextMessage) => Unit) extends Thread {
val Tag: String = "TransferThread"
val InStream: InputStream =
try {
socket.getInputStream()
socket.getInputStream
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
@ -29,7 +29,7 @@ class TransferThread(device: Device, socket: BluetoothSocket,
val OutStream: OutputStream =
try {
socket.getOutputStream()
socket.getOutputStream
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
@ -37,28 +37,26 @@ class TransferThread(device: Device, socket: BluetoothSocket,
}
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
while (true) {
try {
InStream.read(buffer)
val msg: Message = Message.fromByteArray(buffer)
onReceive(device.id, msg)
val msg = TextMessage.fromStream(InStream)
onReceive(msg)
} catch {
case e: IOException =>
Log.e(Tag, "Disconnected from device", e);
Log.e(Tag, "Disconnected from device", e)
return
}
}
}
def send(message: Message): Unit = {
def send(message: TextMessage): Unit = {
try {
OutStream.write(message.toByteArray())
message.write(OutStream)
} catch {
case e: IOException =>
Log.e(Tag, "Failed to write message", e);
Log.e(Tag, "Failed to write message", e)
}
}

View File

@ -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
}

View File

@ -3,22 +3,25 @@ package com.nutomic.ensichat.fragments
import android.app.ListFragment
import android.content.{ComponentName, Context, Intent, ServiceConnection}
import android.os.{Bundle, IBinder}
import android.util.Log
import android.view.{LayoutInflater, View, ViewGroup}
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.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 final val mChatServiceConnection: ServiceConnection = new ServiceConnection {
private final val ChatServiceConnection: ServiceConnection = new ServiceConnection {
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
val binder: ChatServiceBinder = iBinder.asInstanceOf[ChatServiceBinder]
chatService = binder.getService()
chatService.registerDeviceListener(onDeviceConnected)
chatService = binder.getService
chatService.registerDeviceListener(ContactsFragment.this)
}
override def onServiceDisconnected(componentName: ComponentName): Unit = {
@ -31,7 +34,7 @@ class ContactsFragment extends ListFragment {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
savedInstanceState: Bundle): View = {
val view: View = inflater.inflate(R.layout.fragment_contacts, container, false)
return view
view
}
override def onCreate(savedInstanceState: Bundle): Unit = {
@ -40,19 +43,21 @@ class ContactsFragment extends ListFragment {
adapter = new DevicesAdapter(getActivity)
setListAdapter(adapter)
getActivity.bindService(new Intent(getActivity, classOf[ChatService]),
mChatServiceConnection, Context.BIND_AUTO_CREATE)
ChatServiceConnection, Context.BIND_AUTO_CREATE)
}
override def onDestroy(): Unit = {
super.onDestroy()
getActivity.unbindService(mChatServiceConnection)
chatService.unregisterDeviceListener(onDeviceConnected)
getActivity.unbindService(ChatServiceConnection)
}
/**
* 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 }
getActivity.runOnUiThread(new Runnable {
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 = {
chatService.send(adapter.getItem(position).id, new Message("Ping"))
getActivity.asInstanceOf[MainActivity].openChat(adapter.getItem(position).id)
}
}

View File

@ -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
}

View File

@ -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 = {
}
}

View File

@ -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 + ")"
}

View File

@ -1,17 +1,21 @@
package com.nutomic.ensichat.util
import android.content.Context
import android.view.{ViewGroup, View}
import android.widget.{TextView, ArrayAdapter}
import android.view.{View, ViewGroup}
import android.widget.{ArrayAdapter, TextView}
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 = {
val view = super.getView(position, convertView, parent)
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
title.setText(getItem(position).name)
return view
view
}
}

View File

@ -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
}
}