Addded basic ping functionality between devices.

This commit is contained in:
Felix Ableitner 2014-10-22 18:43:49 +03:00
parent 6c4fe96f10
commit 70fec6ad08
8 changed files with 372 additions and 42 deletions

View File

@ -9,26 +9,38 @@ import android.widget.Toast
import com.nutomic.ensichat.R
import com.nutomic.ensichat.bluetooth.ChatService
/**
* Main activity, holds fragments and requests bluetooth to be enabled.
*/
class MainActivity extends Activity {
private final val REQUEST_ENABLE_BLUETOOTH = 1
private final val RequestEnableBluetooth = 1
/**
* Initializes layout, starts service and requests bluetooth.
*/
override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startService(new Intent(this, classOf[ChatService]))
val intent: Intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(intent, REQUEST_ENABLE_BLUETOOTH)
startActivityForResult(intent, RequestEnableBluetooth)
}
/**
* Initializes menu.
*/
override def onCreateOptionsMenu(menu: Menu): Boolean = {
getMenuInflater().inflate(R.menu.main, menu)
return true
}
/**
* Exits with error if bluetooth was not enabled,
*/
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = {
requestCode match {
case REQUEST_ENABLE_BLUETOOTH =>
case RequestEnableBluetooth =>
if (resultCode != Activity.RESULT_OK) {
Toast.makeText(this, R.string.bluetooth_required, Toast.LENGTH_LONG).show()
finish()
@ -36,6 +48,9 @@ class MainActivity extends Activity {
}
}
/**
* Menu click handler.
*/
override def onOptionsItemSelected(item: MenuItem): Boolean = {
item.getItemId match {
case R.id.exit =>

View File

@ -1,67 +1,145 @@
package com.nutomic.ensichat.bluetooth
import java.util.UUID
import android.app.Service
import android.bluetooth.{BluetoothDevice, BluetoothAdapter}
import android.content.{Context, BroadcastReceiver, IntentFilter, Intent}
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.IBinder
import android.util.Log
import com.nutomic.ensichat.bluetooth.ChatService.DeviceListener
import android.widget.Toast
import com.nutomic.ensichat.{Message, R}
import android.os.Handler
import scala.collection.immutable.{HashMap, Set}
object ChatService {
trait DeviceListener {
def onDeviceConnected(device: Device): Unit
}
/**
* Bluetooth service UUID version 5, created with namespace URL and "ensichat.nutomic.com".
*/
val appUuid: UUID = UUID.fromString("8ed52b7a-4501-5348-b054-3d94d004656e")
}
class ChatService extends Service {
private val TAG = "ChatService"
private val Tag = "ChatService"
private final val mBinder = new ChatServiceBinder(this)
private val SCAN_INTERVAL: Int = 5000
private var mBluetoothAdapter: BluetoothAdapter = _
private final val Binder = new ChatServiceBinder(this)
private var mDeviceListener: DeviceListener = _
private var bluetoothAdapter: BluetoothAdapter = _
private var deviceListener: Set[Map[Device.ID, Device] => Unit] =
Set[Map[Device.ID, Device] => Unit]()
private var devices: HashMap[Device.ID, Device] = new HashMap[Device.ID, Device]()
private var connections: HashMap[Device.ID, TransferThread] =
new HashMap[Device.ID, TransferThread]()
private var ListenThread: ListenThread = _
private var isDestroyed = false
private val MainHandler: Handler = new Handler()
/**
* Initializes BroadcastReceiver for discovery, starts discovery and listens for connections.
*/
override def onCreate(): Unit = {
super.onCreate()
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
var filter: IntentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND)
registerReceiver(mReceiver, filter)
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
registerReceiver(mReceiver, filter)
doDiscovery()
Log.i(Tag, "Discovery started")
discover()
ListenThread = new ListenThread(getString(R.string.app_name), bluetoothAdapter, onConnected)
ListenThread.start()
}
override def onBind(intent: Intent): IBinder = {
return mBinder
return Binder
}
def doDiscovery() {
// If we're already discovering, stop it.
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery()
override def onDestroy(): Unit = {
ListenThread.cancel()
isDestroyed = true
}
/**
* Stops any current discovery, then starts a new one, recursively until service is stopped.
*/
def discover(): Unit = {
if (isDestroyed)
return
if (!bluetoothAdapter.isDiscovering()) {
Log.v(Tag, "Running discovery")
bluetoothAdapter.startDiscovery()
}
mBluetoothAdapter.startDiscovery()
Log.i(TAG, "Discovery started")
MainHandler.postDelayed(new Runnable {
override def run(): Unit = discover()
}, SCAN_INTERVAL)
}
/**
* Receives newly discovered devices and connects to them.
*/
private final def mReceiver: BroadcastReceiver = new BroadcastReceiver() {
override def onReceive(context: Context, intent: Intent) {
intent.getAction() match {
case BluetoothDevice.ACTION_FOUND =>
val btDevice: BluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
mDeviceListener.onDeviceConnected(new Device(btDevice))
val device: Device =
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
devices = devices + (device.id -> device)
new ConnectThread(device, onConnected).start()
deviceListener.foreach(d => d(devices))
case _ =>
}
}
}
def registerDeviceListener(listener: DeviceListener): Unit = {
mDeviceListener = listener
/**
* Registers a listener that is called whenever a new device is connected.
*/
def registerDeviceListener(listener: Map[Device.ID, Device] => Unit): Unit = {
deviceListener = deviceListener + listener
listener(devices)
}
/**
* Unregisters a device listener.
*/
def unregisterDeviceListener(listener: Map[Device.ID, Device] => Unit): Unit = {
deviceListener = deviceListener - listener
}
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))
connections(device.id).start()
deviceListener.foreach(d => d(devices))
}
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()
})
}
}

View File

@ -0,0 +1,37 @@
package com.nutomic.ensichat.bluetooth
import java.io.IOException
import android.bluetooth.BluetoothSocket
import android.util.Log
/**
* Attempts to connect to another device and calls [[onConnected]] on success.
*/
class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit)
extends Thread {
val Tag = "ConnectThread"
val socket: BluetoothSocket =
device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid)
override def run(): Unit = {
try {
socket.connect()
} catch {
case e: IOException =>
try {
socket.close()
} catch {
case e2: IOException =>
Log.e(Tag, "Failed to close socket", e2);
}
return;
}
Log.i(Tag, "Successfully connected to device " + device.name)
onConnected(device, socket)
}
}

View File

@ -2,8 +2,34 @@ package com.nutomic.ensichat.bluetooth
import android.bluetooth.BluetoothDevice
class Device(mBluetoothDevice: BluetoothDevice) {
object Device {
def name = mBluetoothDevice.getName()
/**
* Holds bluetooth device IDs, which are just wrapped addresses (used for type safety).
*
* @param Id A bluetooth device address.
*/
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
}
}
}
}
/**
* Holds information about a remote bluetooth device.
*/
class Device(BluetoothDevice: BluetoothDevice, Connected: Boolean) {
def id = new Device.ID(bluetoothDevice.getAddress)
def name = BluetoothDevice.getName
def connected = Connected
def bluetoothDevice = BluetoothDevice
}

View File

@ -0,0 +1,52 @@
package com.nutomic.ensichat.bluetooth
import java.io.IOException
import android.bluetooth.{BluetoothAdapter, BluetoothServerSocket, BluetoothSocket}
import android.util.Log
/**
* Listens for incoming connections from other devices.
*/
class ListenThread(name: String, adapter: BluetoothAdapter,
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
val Tag: String = "ListenThread"
val ServerSocket: BluetoothServerSocket =
try {
adapter.listenUsingInsecureRfcommWithServiceRecord(name, ChatService.appUuid)
} catch {
case e: IOException =>
Log.e(Tag, "Failed to create listener", e)
null
}
override def run(): Unit = {
var socket: BluetoothSocket = null
while (true) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = ServerSocket.accept()
} catch {
case e: IOException =>
Log.e(Tag, "Failed to accept new connection", e)
}
val device: Device = new Device(socket.getRemoteDevice, true)
onConnected(device, socket)
}
}
def cancel(): Unit = {
try {
ServerSocket.close()
} catch {
case e: IOException =>
Log.e(Tag, "Failed to close listener", e)
}
}
}

View File

@ -0,0 +1,74 @@
package com.nutomic.ensichat.bluetooth
import java.io.{OutputStream, InputStream}
import android.bluetooth.BluetoothSocket
import android.util.Log
import com.nutomic.ensichat.Message
import java.io.IOException
/**
* Transfers data between connnected devices.
*
* @param device The bluetooth device to interact with.
* @param socket An open socket to the given device.
* @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 {
val Tag: String = "TransferThread"
val InStream: InputStream =
try {
socket.getInputStream()
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
null
}
val OutStream: OutputStream =
try {
socket.getOutputStream()
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
null
}
override def run(): Unit = {
var buffer: Array[Byte] = new Array(1024)
// Keep listening to the InputStream while connected
while (true) {
try {
InStream.read(buffer)
val msg: Message = Message.fromByteArray(buffer)
onReceive(device.id, msg)
} catch {
case e: IOException =>
Log.e(Tag, "Disconnected from device", e);
return
}
}
}
def send(message: Message): Unit = {
try {
OutStream.write(message.toByteArray())
} catch {
case e: IOException =>
Log.e(Tag, "Failed to write message", e);
}
}
def cancel(): Unit = {
try {
socket.close()
} catch {
case e: IOException =>
Log.e(Tag, "Failed to close socket", e);
}
}
}

View File

@ -3,30 +3,30 @@ 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
import com.nutomic.ensichat.R
import com.nutomic.ensichat.bluetooth.ChatService.DeviceListener
import android.widget.{ArrayAdapter, ListView}
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
import com.nutomic.ensichat.util.DevicesAdapter
import com.nutomic.ensichat.{Message, R}
class ContactsFragment extends ListFragment with DeviceListener {
class ContactsFragment extends ListFragment {
private var mChatService: ChatService = _
private var chatService: ChatService = _
private final val mChatServiceConnection: ServiceConnection = new ServiceConnection {
override def onServiceConnected(componentName: ComponentName, iBinder: IBinder): Unit = {
val binder: ChatServiceBinder = iBinder.asInstanceOf[ChatServiceBinder]
mChatService = binder.getService()
mChatService.registerDeviceListener(ContactsFragment.this)
chatService = binder.getService()
chatService.registerDeviceListener(onDeviceConnected)
}
override def onServiceDisconnected(componentName: ComponentName): Unit = {
mChatService = null
chatService = null
}
}
private var mAdapter: ArrayAdapter[Device] = _
private var adapter: ArrayAdapter[Device] = _
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
savedInstanceState: Bundle): View = {
@ -37,8 +37,8 @@ class ContactsFragment extends ListFragment with DeviceListener {
override def onCreate(savedInstanceState: Bundle): Unit = {
super.onCreate(savedInstanceState)
mAdapter = new DevicesAdapter(getActivity)
setListAdapter(mAdapter)
adapter = new DevicesAdapter(getActivity)
setListAdapter(adapter)
getActivity.bindService(new Intent(getActivity, classOf[ChatService]),
mChatServiceConnection, Context.BIND_AUTO_CREATE)
}
@ -46,10 +46,27 @@ class ContactsFragment extends ListFragment with DeviceListener {
override def onDestroy(): Unit = {
super.onDestroy()
getActivity.unbindService(mChatServiceConnection)
chatService.unregisterDeviceListener(onDeviceConnected)
}
override def onDeviceConnected(device: Device): Unit = {
mAdapter.add(device)
/**
* Displays all connected devices in the listview.
*/
def onDeviceConnected(devices: Map[Device.ID, Device]): Unit = {
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))
}
})
}
/**
* Sends a ping message to the clicked device.
*/
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit = {
chatService.send(adapter.getItem(position).id, new Message("Ping"))
}
}

View File

@ -0,0 +1,31 @@
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
}