Addded basic ping functionality between devices.
This commit is contained in:
parent
6c4fe96f10
commit
70fec6ad08
8 changed files with 372 additions and 42 deletions
|
@ -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 =>
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
Reference in a new issue