Slide 1

Slide 1 text

The world of Android wireless communications without Internet DroidKaigi 2022 @fushiroyama

Slide 2

Slide 2 text

About me Fumihiko Shiroyama Sr. Software Engineer at Microsoft Father of two girls PhD Student at JAIST

Slide 3

Slide 3 text

Agenda • Overview of wireless communications without Internet • Brief description of how to use them • General guidelines on how to use each one differently • Steps to more advanced topics

Slide 4

Slide 4 text

Overview of wireless communications without Internet

Slide 5

Slide 5 text

Wireless communications available for Android • Bluetooth Classic • Wi-Fi • Bluetooth Low Energy • NFC, etc..

Slide 6

Slide 6 text

Bluetooth Classic • Bluetooth version 1.0 - 3.0 • a.k.a. Bluetooth Basic Rate/Enhanced Data Rate (BR/EDR) • 2.4 GHz band • 79ch frequency-hopping spread spectrum • main/follower architecture (1-to-max 7) • Widely used to connect digital devices and exchange information

Slide 7

Slide 7 text

Wi-Fi • IEEE 802.11 • Wireless Ethernet • Typically used in the star topology, but Wi-Fi Direct is available for P2P communication

Slide 8

Slide 8 text

Bluetooth Low Energy • Bluetooth 4.0 or higher • Not compatible with Bluetooth Classic • 2.4 GHz band • 40ch frequency-hopping spread spectrum • Very low power consumption • main/follower architecture (1 to N*) * unde fi ned or implementation dependent

Slide 9

Slide 9 text

NFC • Near- fi eld communication (NFC) is undoubtedly a form of short- range wireless communication, but it is typically ultra-short-range, less than 4 cm • Not covered in this session

Slide 10

Slide 10 text

Brief description of how to use them

Slide 11

Slide 11 text

Sample code repository • https://github.com/shiroyama/DroidKaigi2022

Slide 12

Slide 12 text

Wi-Fi Direct

Slide 13

Slide 13 text

Wi-Fi Direct • Wi-Fi communication without an access point • Can be handled exactly like a TCP socket once the connection is established

Slide 14

Slide 14 text

Permissions

Slide 15

Slide 15 text

Wi-Fi manager and channel private lateinit var manager: WifiP2pManager private lateinit var channel: WifiP2pManager.Channel manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager channel = manager.initialize(applicationContext, mainLooper, null)

Slide 16

Slide 16 text

Intent fi lter for receiving Wi-Fi status private val intentFilter: IntentFilter = object : IntentFilter() { init { addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) } }

Slide 17

Slide 17 text

BroadcastReceiver for Wi-Fi status override fun onReceive(context: Context, intent: Intent) { when (intent.action) { WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {} WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {} WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {} WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {} } }

Slide 18

Slide 18 text

BroadcastReceiver for Wi-Fi status override fun onReceive(context: Context, intent: Intent) { when (intent.action) { WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {} WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {} WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {} WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {} } }

Slide 19

Slide 19 text

Register BroadcastReceiver override fun onResume() { super.onResume() receiver = WiFiDirectBroadcastReceiver(manager, channel, this) registerReceiver(receiver, intentFilter) } override fun onPause() { unregisterReceiver(receiver) super.onPause() }

Slide 20

Slide 20 text

Register BroadcastReceiver override fun onResume() { super.onResume() receiver = WiFiDirectBroadcastReceiver(manager, channel, this) registerReceiver(receiver, intentFilter) } override fun onPause() { unregisterReceiver(receiver) super.onPause() }

Slide 21

Slide 21 text

Discover peers manager.discoverPeers(channel, object : WifiP2pManager.ActionListener { override fun onSuccess() { Log.d(TAG, "discoverPeers onSuccess") } override fun onFailure(i: Int) { Log.e(TAG, "discoverPeers onFailure: $i") } })

Slide 22

Slide 22 text

When peer found WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { }

Slide 23

Slide 23 text

Request peer list WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { manager.requestPeers(channel) { wifiP2pDeviceList: WifiP2pDeviceList -> val deviceList = wifiP2pDeviceList.deviceList } }

Slide 24

Slide 24 text

Request peer list WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { manager.requestPeers(channel) { wifiP2pDeviceList: WifiP2pDeviceList -> val deviceList = wifiP2pDeviceList.deviceList } }

Slide 25

Slide 25 text

Request peer list WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { manager.requestPeers(channel) { wifiP2pDeviceList: WifiP2pDeviceList -> val deviceList = wifiP2pDeviceList.deviceList } }

Slide 26

Slide 26 text

Connect to one of the peers fun connect(device: WifiP2pDevice) { val config = WifiP2pConfig() config.deviceAddress = device.deviceAddress config.wps.setup = WpsInfo.PBC manager.connect(channel, config, object : WifiP2pManager.ActionListener { override fun onSuccess() { Log.d(TAG, "connect onSuccess") } override fun onFailure(i: Int) { Log.e(TAG, "connect onFailure: $i") } }) }

Slide 27

Slide 27 text

Connect to one of the peers fun connect(device: WifiP2pDevice) { val config = WifiP2pConfig() config.deviceAddress = device.deviceAddress config.wps.setup = WpsInfo.PBC manager.connect(channel, config, object : WifiP2pManager.ActionListener { override fun onSuccess() { Log.d(TAG, "connect onSuccess") } override fun onFailure(i: Int) { Log.e(TAG, "connect onFailure: $i") } }) }

Slide 28

Slide 28 text

Connect to one of the peers fun connect(device: WifiP2pDevice) { val config = WifiP2pConfig() config.deviceAddress = device.deviceAddress config.wps.setup = WpsInfo.PBC manager.connect(channel, config, object : WifiP2pManager.ActionListener { override fun onSuccess() { Log.d(TAG, "connect onSuccess") } override fun onFailure(i: Int) { Log.e(TAG, "connect onFailure: $i") } }) }

Slide 29

Slide 29 text

When connection status changes WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { }

Slide 30

Slide 30 text

Get connection details WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { val networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) val wifiP2pGroup = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP) if (networkInfo?.isConnected == true) { manager.requestConnectionInfo(channel) { wifiP2pInfo: WifiP2pInfo -> } } }

Slide 31

Slide 31 text

Get connection details WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { val networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) val wifiP2pGroup = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP) if (networkInfo?.isConnected == true) { manager.requestConnectionInfo(channel) { wifiP2pInfo: WifiP2pInfo -> } } }

Slide 32

Slide 32 text

Connected if (wifiP2pInfo.groupFormed && wifiP2pGroup?.isGroupOwner == true) { serverThread = ServerThread() serverThread.start() } else if (wifiP2pInfo.groupFormed) { clientThread = ClientThread() clientThread.start() } else { isConnected = false }

Slide 33

Slide 33 text

Connected if (wifiP2pInfo.groupFormed && wifiP2pGroup?.isGroupOwner == true) { serverThread = ServerThread() serverThread.start() } else if (wifiP2pInfo.groupFormed) { clientThread = ClientThread() clientThread.start() } else { isConnected = false }

Slide 34

Slide 34 text

Connected if (wifiP2pInfo.groupFormed && wifiP2pGroup?.isGroupOwner == true) { serverThread = ServerThread() serverThread.start() } else if (wifiP2pInfo.groupFormed) { clientThread = ClientThread() clientThread.start() } else { isConnected = false }

Slide 35

Slide 35 text

Connected if (wifiP2pInfo.groupFormed && wifiP2pGroup?.isGroupOwner == true) { serverThread = ServerThread() serverThread.start() } else if (wifiP2pInfo.groupFormed) { clientThread = ClientThread() clientThread.start() } else { isConnected = false }

Slide 36

Slide 36 text

ServerThread val serverSocket = ServerSocket(SOCKET_PORT) // blocks until a connection is made val socket: Socket = serverSocket.accept() val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 37

Slide 37 text

ServerThread val serverSocket = ServerSocket(SOCKET_PORT) // blocks until a connection is made val socket: Socket = serverSocket.accept() val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 38

Slide 38 text

ServerThread val serverSocket = ServerSocket(SOCKET_PORT) // blocks until a connection is made val socket: Socket = serverSocket.accept() val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 39

Slide 39 text

ServerThread val serverSocket = ServerSocket(SOCKET_PORT) // blocks until a connection is made val socket: Socket = serverSocket.accept() val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 40

Slide 40 text

ClientThread val socket = Socket() // blocks until a connection is made socket.connect( InetSocketAddress(wifiP2pInfo.groupOwnerAddress, SOCKET_PORT), SOCKET_TIMEOUT ) val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 41

Slide 41 text

ClientThread val socket = Socket() // blocks until a connection is made socket.connect( InetSocketAddress(wifiP2pInfo.groupOwnerAddress, SOCKET_PORT), SOCKET_TIMEOUT ) val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 42

Slide 42 text

ClientThread val socket = Socket() // blocks until a connection is made socket.connect( InetSocketAddress(wifiP2pInfo.groupOwnerAddress, SOCKET_PORT), SOCKET_TIMEOUT ) val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 43

Slide 43 text

ClientThread val socket = Socket() // blocks until a connection is made socket.connect( InetSocketAddress(wifiP2pInfo.groupOwnerAddress, SOCKET_PORT), SOCKET_TIMEOUT ) val inputStream: InputStream = socket.getInputStream() val outputStream: OutputStream = socket.getOutputStream()

Slide 44

Slide 44 text

Wi-Fi Direct • At this point, everything is just socket programming🔌 • Read message from the InputStream and write whatever you want to the OutputStream

Slide 45

Slide 45 text

Writing val executorService = Executors.newSingleThreadExecutor() executorService.submit { try { val inputStream = activity.resources.openRawResource(R.raw.sample) val buffer = ByteArray(8192) while (inputStream.read(buffer).also { length = it } != -1) { outputStream.write(buffer) } } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 46

Slide 46 text

Writing val executorService = Executors.newSingleThreadExecutor() executorService.submit { try { val inputStream = activity.resources.openRawResource(R.raw.sample) val buffer = ByteArray(8192) while (inputStream.read(buffer).also { length = it } != -1) { outputStream.write(buffer) } } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 47

Slide 47 text

Writing val executorService = Executors.newSingleThreadExecutor() executorService.submit { try { val inputStream = activity.resources.openRawResource(R.raw.sample) val buffer = ByteArray(8192) while (inputStream.read(buffer).also { length = it } != -1) { outputStream.write(buffer) } } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 48

Slide 48 text

Reading while (socket != null && !socket.isClosed) { // keep reading while connected }

Slide 49

Slide 49 text

Reading val buffer = ByteArray(8192) try { val filename = System.currentTimeMillis().toString() + ".jpg" val fileOutputStream = context.openFileOutput(filename, Context.MODE_PRIVATE) var length: Int while (inputStream.read(buffer).also { length = it } != -1) { fileOutputStream.write(buffer, 0, length) } fileOutputStream.close() } catch (e: IOException) { Log.e(TAG, e.message, e) }

Slide 50

Slide 50 text

Reading val buffer = ByteArray(8192) try { val filename = System.currentTimeMillis().toString() + ".jpg" val fileOutputStream = context.openFileOutput(filename, Context.MODE_PRIVATE) var length: Int while (inputStream.read(buffer).also { length = it } != -1) { fileOutputStream.write(buffer, 0, length) } fileOutputStream.close() } catch (e: IOException) { Log.e(TAG, e.message, e) }

Slide 51

Slide 51 text

Reading val buffer = ByteArray(8192) try { val filename = System.currentTimeMillis().toString() + ".jpg" val fileOutputStream = context.openFileOutput(filename, Context.MODE_PRIVATE) var length: Int while (inputStream.read(buffer).also { length = it } != -1) { fileOutputStream.write(buffer, 0, length) } fileOutputStream.close() } catch (e: IOException) { Log.e(TAG, e.message, e) }

Slide 52

Slide 52 text

Tips • TCP has no such thing as a stream boundary • Messages may be sent and received in batches • If you keep the socket open and continue the bidirectional communication, you need a protocol. For example, put the message length in the fi rst few bytes.

Slide 53

Slide 53 text

Send messages with size fun writeMessage(message: String) { val messageBytes = message.toByteArray(StandardCharsets.UTF_8) val length = messageBytes.size val lengthBytes = ByteBuffer.allocate(4).putInt(length).array() // writing the length of the message write(lengthBytes) // writing the message delimiter val delimiterBytes = ByteBuffer.allocate(2).putChar(':').array() write(delimiterBytes) // writing the message itself write(messageBytes) }

Slide 54

Slide 54 text

Send messages with size fun writeMessage(message: String) { val messageBytes = message.toByteArray(StandardCharsets.UTF_8) val length = messageBytes.size val lengthBytes = ByteBuffer.allocate(4).putInt(length).array() // writing the length of the message write(lengthBytes) // writing the message delimiter val delimiterBytes = ByteBuffer.allocate(2).putChar(':').array() write(delimiterBytes) // writing the message itself write(messageBytes) }

Slide 55

Slide 55 text

Send messages with size fun writeMessage(message: String) { val messageBytes = message.toByteArray(StandardCharsets.UTF_8) val length = messageBytes.size val lengthBytes = ByteBuffer.allocate(4).putInt(length).array() // writing the length of the message write(lengthBytes) // writing the message delimiter val delimiterBytes = ByteBuffer.allocate(2).putChar(':').array() write(delimiterBytes) // writing the message itself write(messageBytes) }

Slide 56

Slide 56 text

Receive message with size val lengthBuffer = ByteArray(4) inputStream.read(lengthBuffer) val length = ByteBuffer.wrap(lengthBuffer).int val delimiterBuffer = ByteArray(2) inputStream.read(delimiterBuffer) val delimiter = ByteBuffer.wrap(delimiterBuffer).char

Slide 57

Slide 57 text

Receive message with size val lengthBuffer = ByteArray(4) inputStream.read(lengthBuffer) val length = ByteBuffer.wrap(lengthBuffer).int val delimiterBuffer = ByteArray(2) inputStream.read(delimiterBuffer) val delimiter = ByteBuffer.wrap(delimiterBuffer).char

Slide 58

Slide 58 text

Receive message with size val pageSize = (length + BUFFER_SIZE - 1) / BUFFER_SIZE var totalBytes = 0 val messageBuffer = ByteArray(length) for (i in 0 until pageSize) { val limit = if (i == pageSize - 1) length - totalBytes else BUFFER_SIZE val incomingBytes = inputStream.read(INCOMING_BUFF, 0, limit) System.arraycopy(INCOMING_BUFF, 0, messageBuffer, totalBytes, incomingBytes) totalBytes += incomingBytes Log.d(TAG, "read(): totalBytes after reading = $totalBytes") }

Slide 59

Slide 59 text

Receive message with size val pageSize = (length + BUFFER_SIZE - 1) / BUFFER_SIZE var totalBytes = 0 val messageBuffer = ByteArray(length) for (i in 0 until pageSize) { val limit = if (i == pageSize - 1) length - totalBytes else BUFFER_SIZE val incomingBytes = inputStream.read(INCOMING_BUFF, 0, limit) System.arraycopy(INCOMING_BUFF, 0, messageBuffer, totalBytes, incomingBytes) totalBytes += incomingBytes Log.d(TAG, "read(): totalBytes after reading = $totalBytes") }

Slide 60

Slide 60 text

Receive message with size val pageSize = (length + BUFFER_SIZE - 1) / BUFFER_SIZE var totalBytes = 0 val messageBuffer = ByteArray(length) for (i in 0 until pageSize) { val limit = if (i == pageSize - 1) length - totalBytes else BUFFER_SIZE val incomingBytes = inputStream.read(INCOMING_BUFF, 0, limit) System.arraycopy(INCOMING_BUFF, 0, messageBuffer, totalBytes, incomingBytes) totalBytes += incomingBytes Log.d(TAG, "read(): totalBytes after reading = $totalBytes") }

Slide 61

Slide 61 text

Wi-Fi Direct summary • Wi-Fi Direct enables peer-to-peer communications without an access point on all devices that support Wi-Fi • Once the connection is established, it is simple TCP socket communication. Both text and binary can be sent and received • Pro's and con's to other wireless communications will be discussed later

Slide 62

Slide 62 text

Bluetooth Classic

Slide 63

Slide 63 text

Bluetooth Classic • main/follower (1 to 1)

Slide 64

Slide 64 text

Bluetooth Classic • main/follower (1 to max 7)

Slide 65

Slide 65 text

Bluetooth Classic • Pro fi le: wireless interface speci fi cation for Bluetooth-based communication between device • Headset • Advanced Audio Distribution Pro fi le (A2DP) • Health Device • https://developer.android.com/guide/topics/connectivity/bluetooth/ pro fi les • This talk will NOT cover those topics

Slide 66

Slide 66 text

Bluetooth Classic • L2CAP (Logical Link Control and Adaptive Protocol) • In the OSI reference model, it is roughly equivalent to the data link layer (layer 2) • RFCOMM (Radio Frequency Communication) • A protocol that emulates the RS-232C serial port on an L2CAP. • It has packet reception con fi rmation and retransmission like TCP. • In the OSI reference model, it is roughly equivalent to the transport layer (layer 4) • RFCOMM sockets are the most common way to communicate in Android's Bluetooth API

Slide 67

Slide 67 text

Permissions

Slide 68

Slide 68 text

Permissions

Slide 69

Slide 69 text

Check Bluetooth availability val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter == null) { // Device doesn't support Bluetooth } if (bluetoothAdapter?.isEnabled == false) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }

Slide 70

Slide 70 text

Check Bluetooth availability val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter == null) { // Device doesn't support Bluetooth } if (bluetoothAdapter?.isEnabled == false) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }

Slide 71

Slide 71 text

Check Bluetooth availability val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter == null) { // Device doesn't support Bluetooth } if (bluetoothAdapter?.isEnabled == false) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }

Slide 72

Slide 72 text

BroadcastReceiver to get found device private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { BluetoothDevice.ACTION_FOUND -> { val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) device?.let { val deviceName = it.name val deviceAddress = it.address } } } } }

Slide 73

Slide 73 text

BroadcastReceiver to get found device private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { BluetoothDevice.ACTION_FOUND -> { val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) device?.let { val deviceName = it.name val deviceAddress = it.address } } } } }

Slide 74

Slide 74 text

Register BroadcastReceiver override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) val intentFilter = IntentFilter(BluetoothDevice.ACTION_FOUND) registerReceiver(receiver, intentFilter) } override fun onDestroy() { unregisterReceiver(receiver) super.onDestroy() }

Slide 75

Slide 75 text

Make the device discoverable val intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, DISCOVERABLE_DURATION) startActivityForResult(intent, REQUEST_CODE)

Slide 76

Slide 76 text

Make the device discoverable val intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, DISCOVERABLE_DURATION) startActivityForResult(intent, REQUEST_CODE)

Slide 77

Slide 77 text

Start discovery if (bluetoothAdapter.isDiscovering) { bluetoothAdapter.cancelDiscovery() } val discoveryResult = bluetoothAdapter.startDiscovery()

Slide 78

Slide 78 text

Start discovery if (bluetoothAdapter.isDiscovering) { bluetoothAdapter.cancelDiscovery() } val discoveryResult = bluetoothAdapter.startDiscovery()

Slide 79

Slide 79 text

ServerThread class BTServerThread : Thread() { private lateinit var bluetoothServerSocket: BluetoothServerSocket init { try { bluetoothServerSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord( BT_NAME, BT_UUID ) } catch (e: IOException) {} } override fun run() { try { // blocks until connection val bluetooth: BluetoothSocket = bluetoothServerSocket.accept() } catch (e: IOException) {} }

Slide 80

Slide 80 text

ServerThread class BTServerThread : Thread() { private lateinit var bluetoothServerSocket: BluetoothServerSocket init { try { bluetoothServerSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord( BT_NAME, BT_UUID ) } catch (e: IOException) {} } override fun run() { try { // blocks until connection val bluetooth: BluetoothSocket = bluetoothServerSocket.accept() } catch (e: IOException) {} }

Slide 81

Slide 81 text

ServerThread class BTServerThread : Thread() { private lateinit var bluetoothServerSocket: BluetoothServerSocket init { try { bluetoothServerSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord( BT_NAME, BT_UUID ) } catch (e: IOException) {} } override fun run() { try { // blocks until connection val bluetooth: BluetoothSocket = bluetoothServerSocket.accept() } catch (e: IOException) {} }

Slide 82

Slide 82 text

Connect to server fun connect(bluetoothDevice: BluetoothDevice) { btClientThread = BTClientThread(bluetoothDevice) btClientThread.start() }

Slide 83

Slide 83 text

ClientThread class BTClientThread(private val bluetoothDevice: BluetoothDevice) : Thread() { private lateinit var bluetoothSocket: BluetoothSocket init { try { bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(BT_UUID) } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 84

Slide 84 text

ClientThread class BTClientThread(private val bluetoothDevice: BluetoothDevice) : Thread() { private lateinit var bluetoothSocket: BluetoothSocket init { try { bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(BT_UUID) } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 85

Slide 85 text

ClientThread override fun run() { try { if (bluetoothAdapter.isDiscovering) { bluetoothAdapter.cancelDiscovery() } // blocks until connection bluetoothSocket.connect() ... } catch (e: IOException) {} }

Slide 86

Slide 86 text

ClientThread override fun run() { try { if (bluetoothAdapter.isDiscovering) { bluetoothAdapter.cancelDiscovery() } // blocks until connection bluetoothSocket.connect() ... } catch (e: IOException) {} }

Slide 87

Slide 87 text

BluetoothSocket val inputStream: InputStream = bluetoothSocket.inputStream val outputStream: OutputStream = bluetoothSocket.outputStream Same as TCP Socket!!

Slide 88

Slide 88 text

Bluetooth Classic summary • BluetoothSocket can be handled exactly like a TCP Socket • Thus, exactly the same applies when dealing with streams • Pro's and con's to other wireless communications will be discussed later

Slide 89

Slide 89 text

Bluetooth Low Energy

Slide 90

Slide 90 text

Bluetooth Low Energy • a.k.a. Bluetooth LE or BLE • main/follower (1 to 1) • main is called Central, follower is called Peripheral

Slide 91

Slide 91 text

Bluetooth Low Energy • central/peripheral (1 to N)

Slide 92

Slide 92 text

Bluetooth Low Energy • Central and Peripheral are connected to form a main/follower relationship • Peripheral can include a small amount of payload in advertised packets e.g.) iBeacon • After connection, Central and Peripheral communicate using a method called GATT (Generic ATTribute pro le)

Slide 93

Slide 93 text

GATT • It de fi nes the way in which Central reads and writes data to and from the Peripheral • Peripheral is essentially a sensor device • A Peripheral has one or more Services. This speci es what functions the peripheral provides • A Service provides one or more Characteristics. This expresses the characteristics of the service.

Slide 94

Slide 94 text

GATT 1FSJQIFSBM UIFSNPNFUFS 4FSWJDF UFNQFSBUVSFJOGPSNBUJPO 66*%EFDFDBE $IBSBDUFSJTUJD UFNQFSBUVSF 66*%CBECFFGDCFFDDGCB 7BMVF 1SPQFSUZ3FBEc/PUJGZ $IBSBDUFSJTUJD FDPNPEF 66*%DGBFECFEBECEB 7BMVF 1SPQFSUZ8SJUF

Slide 95

Slide 95 text

How it works on Android? • Android can serve as Central with 4.3 or later • Android can serve as Peripheral with 5.0 or later • What's the point for Android to become Peripheral? I don't know🤷 The idea is up to you!

Slide 96

Slide 96 text

Two-way communication scenario $FOUSBM 1FSJQIFSBM .FTTBHF 
 4FSWJDF .FTTBHF $IBSBDUFSJTUJD 7BMVFMBTUNFTTBHF 1SPQFSUZ8SJUFc/PUJGZ

Slide 97

Slide 97 text

Two-way communication scenario $FOUSBM 1FSJQIFSBM .FTTBHF 
 4FSWJDF .FTTBHF $IBSBDUFSJTUJD 7BMVFMBTUNFTTBHF 1SPQFSUZ8SJUFc/PUJGZ Hello! Write "Hello!"

Slide 98

Slide 98 text

Two-way communication scenario $FOUSBM 1FSJQIFSBM .FTTBHF 
 4FSWJDF .FTTBHF $IBSBDUFSJTUJD 7BMVFMBTUNFTTBHF 1SPQFSUZ8SJUFc/PUJGZ Ahoy! Notify "Ahoy!" Enable noti fi cation

Slide 99

Slide 99 text

Permissions

Slide 100

Slide 100 text

Check BLE support if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { // BLE is not supported on this device return } val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val bluetoothAdapter = bluetoothManager.adapter if (bluetoothAdapter == null) { // Bluetooth is not supported on this device return }

Slide 101

Slide 101 text

Central 1. Scan Peripherals around 2. Connect to Peripheral 3. Enable Noti fi cation

Slide 102

Slide 102 text

Central - Scan Peripheral bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner bluetoothLeScanner.startScan(scanCallback)

Slide 103

Slide 103 text

Central - Scan Peripheral private val scanCallback: ScanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { super.onScanResult(callbackType, result) val bluetoothDevice = result.device } override fun onBatchScanResults(results: List) { super.onBatchScanResults(results) } override fun onScanFailed(errorCode: Int) { super.onScanFailed(errorCode) Log.e(TAG, String.format("onScanFailed(errorCode = %d)", errorCode)) } }

Slide 104

Slide 104 text

Central - Scan Peripheral private val scanCallback: ScanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { super.onScanResult(callbackType, result) val bluetoothDevice = result.device } override fun onBatchScanResults(results: List) { super.onBatchScanResults(results) } override fun onScanFailed(errorCode: Int) { super.onScanFailed(errorCode) Log.e(TAG, String.format("onScanFailed(errorCode = %d)", errorCode)) } }

Slide 105

Slide 105 text

Central - Connect to Peripheral fun connect(bluetoothDevice: BluetoothDevice) { bluetoothGatt = bluetoothDevice.connectGatt(activity, false, bluetoothGattCallback) }

Slide 106

Slide 106 text

Central - Connect to Peripheral private val bluetoothGattCallback: BluetoothGattCallback = object : BluetoothGattCallback() { override fun onPhyUpdate(...) { } override fun onPhyRead(...) { } override fun onConnectionStateChange(...) { } override fun onServicesDiscovered(...) { } override fun onCharacteristicRead(...) { } override fun onCharacteristicWrite(...) { } override fun onCharacteristicChanged(...) { } override fun onDescriptorRead(...) { } override fun onDescriptorWrite(...) { } override fun onReliableWriteCompleted(...) { } override fun onReadRemoteRssi(...) { } override fun onMtuChanged(...) { } }

Slide 107

Slide 107 text

Central - Connect to Peripheral override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { super.onConnectionStateChange(gatt, status, newState) when (status) { BluetoothProfile.STATE_CONNECTED -> { val discoverServicesResult = gatt.discoverServices() ... } BluetoothProfile.STATE_DISCONNECTED -> { if (bluetoothGatt != null) { bluetoothGatt!!.close() bluetoothGatt = null } isConnected = false } } }

Slide 108

Slide 108 text

Central - Connect to Peripheral override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { super.onConnectionStateChange(gatt, status, newState) when (status) { BluetoothProfile.STATE_CONNECTED -> { val discoverServicesResult = gatt.discoverServices() ... } BluetoothProfile.STATE_DISCONNECTED -> { if (bluetoothGatt != null) { bluetoothGatt!!.close() bluetoothGatt = null } isConnected = false } } }

Slide 109

Slide 109 text

Central - Connect to Peripheral override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { super.onServicesDiscovered(gatt, status) if (status != BluetoothGatt.GATT_SUCCESS) { return } val service = gatt.getService(UUID_SERVICE) if (service == null) { return } val characteristic = service.getCharacteristic(UUID_CHARACTERISTIC) if (characteristic == null) { return } val descriptor = characteristic.getDescriptor(UUID_DESCRIPTOR) if (descriptor == null) { return }

Slide 110

Slide 110 text

Central - Connect to Peripheral override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { super.onServicesDiscovered(gatt, status) if (status != BluetoothGatt.GATT_SUCCESS) { return } val service = gatt.getService(UUID_SERVICE) if (service == null) { return } val characteristic = service.getCharacteristic(UUID_CHARACTERISTIC) if (characteristic == null) { return } val descriptor = characteristic.getDescriptor(UUID_DESCRIPTOR) if (descriptor == null) { return }

Slide 111

Slide 111 text

Central - Connect to Peripheral override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { super.onServicesDiscovered(gatt, status) if (status != BluetoothGatt.GATT_SUCCESS) { return } val service = gatt.getService(UUID_SERVICE) if (service == null) { return } val characteristic = service.getCharacteristic(UUID_CHARACTERISTIC) if (characteristic == null) { return } val descriptor = characteristic.getDescriptor(UUID_DESCRIPTOR) if (descriptor == null) { return }

Slide 112

Slide 112 text

Central - Connect to Peripheral bluetoothGatt = gatt bluetoothGattCharacteristic = characteristic bluetoothLeScanner.stopScan(scanCallback)

Slide 113

Slide 113 text

Central - Enable Noti fi cation val setCharacteristicNotificationResult = bluetoothGatt.setCharacteristicNotification(characteristic, true) if (!setCharacteristicNotificationResult) { return } val descriptorSetValueResult = descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) if (!descriptorSetValueResult) { return } val writeDescriptorResult = bluetoothGatt.writeDescriptor(descriptor) if (!writeDescriptorResult) { return }

Slide 114

Slide 114 text

Central - Enable Noti fi cation val setCharacteristicNotificationResult = bluetoothGatt.setCharacteristicNotification(characteristic, true) if (!setCharacteristicNotificationResult) { return } val descriptorSetValueResult = descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) if (!descriptorSetValueResult) { return } val writeDescriptorResult = bluetoothGatt.writeDescriptor(descriptor) if (!writeDescriptorResult) { return }

Slide 115

Slide 115 text

Central - Enable Noti fi cation val setCharacteristicNotificationResult = bluetoothGatt.setCharacteristicNotification(characteristic, true) if (!setCharacteristicNotificationResult) { return } val descriptorSetValueResult = descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) if (!descriptorSetValueResult) { return } val writeDescriptorResult = bluetoothGatt.writeDescriptor(descriptor) if (!writeDescriptorResult) { return }

Slide 116

Slide 116 text

Central - Enable Noti fi cation override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { val message = characteristic.getStringValue(0) ... } }

Slide 117

Slide 117 text

Central - Enable Noti fi cation override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { val message = characteristic.getStringValue(0) ... } }

Slide 118

Slide 118 text

Peripheral - advertisement 1. Open GATT Server 2. Start advertising

Slide 119

Slide 119 text

Peripheral - Open GATT Server val bluetoothGattDescriptor = BluetoothGattDescriptor( UUID_DESCRIPTOR, BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE ) val bluetoothGattCharacteristic = BluetoothGattCharacteristic( UUID_CHARACTERISTIC, BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattDescriptor.PERMISSION_WRITE or BluetoothGattCharacteristic.PERMISSION_READ )

Slide 120

Slide 120 text

Peripheral - Open GATT Server bluetoothGattCharacteristic.addDescriptor(bluetoothGattDescriptor) val bluetoothGattService = BluetoothGattService(UUID_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY) val addCharacteristicResult = bluetoothGattService.addCharacteristic(bluetoothGattCharacteristic) if (!addCharacteristicResult) { return false } bluetoothGattServer = bluetoothManager.openGattServer(activity, bluetoothGattServerCallback) val addServiceResult = bluetoothGattServer.addService(bluetoothGattService) if (!addServiceResult) { return false } return true

Slide 121

Slide 121 text

Peripheral - Open GATT Server bluetoothGattCharacteristic.addDescriptor(bluetoothGattDescriptor) val bluetoothGattService = BluetoothGattService(UUID_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY) val addCharacteristicResult = bluetoothGattService.addCharacteristic(bluetoothGattCharacteristic) if (!addCharacteristicResult) { return false } bluetoothGattServer = bluetoothManager.openGattServer(activity, bluetoothGattServerCallback) val addServiceResult = bluetoothGattServer.addService(bluetoothGattService) if (!addServiceResult) { return false } return true

Slide 122

Slide 122 text

Peripheral - Open GATT Server private val bluetoothGattServerCallback: BluetoothGattServerCallback = object : BluetoothGattServerCallback() { override fun onConnectionStateChange(...) { } override fun onServiceAdded(...) { } override fun onCharacteristicReadRequest(...) { } override fun onCharacteristicWriteRequest(...) { } override fun onDescriptorReadRequest(...) { } override fun onDescriptorWriteRequest(...) { } override fun onExecuteWrite(...) { } override fun onNotificationSent(...) { } override fun onMtuChanged(...) { } override fun onPhyUpdate(...) { } override fun onPhyRead(...) { } }

Slide 123

Slide 123 text

Peripheral - Open GATT Server override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { super.onConnectionStateChange(device, status, newState) when (status) { BluetoothProfile.STATE_CONNECTED -> { bluetoothLeAdvertiser.stopAdvertising(advertiseCallback) remoteDevice = device isConnected = true } BluetoothProfile.STATE_DISCONNECTED -> { isConnected = false remoteDevice = null } } }

Slide 124

Slide 124 text

Peripheral - Open GATT Server override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { super.onConnectionStateChange(device, status, newState) when (status) { BluetoothProfile.STATE_CONNECTED -> { bluetoothLeAdvertiser.stopAdvertising(advertiseCallback) remoteDevice = device isConnected = true } BluetoothProfile.STATE_DISCONNECTED -> { isConnected = false remoteDevice = null } } }

Slide 125

Slide 125 text

Peripheral - Open GATT Server override fun onDescriptorWriteRequest( device: BluetoothDevice, requestId: Int, descriptor: BluetoothGattDescriptor, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { super.onDescriptorWriteRequest( device, requestId, descriptor, preparedWrite, responseNeeded, offset, value ) if (UUID_DESCRIPTOR == descriptor.uuid) { if (responseNeeded) { bluetoothGattServer.sendResponse( device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value ) } } }

Slide 126

Slide 126 text

Peripheral - Open GATT Server override fun onDescriptorWriteRequest( device: BluetoothDevice, requestId: Int, descriptor: BluetoothGattDescriptor, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { super.onDescriptorWriteRequest( device, requestId, descriptor, preparedWrite, responseNeeded, offset, value ) if (UUID_DESCRIPTOR == descriptor.uuid) { if (responseNeeded) { bluetoothGattServer.sendResponse( device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value ) } } }

Slide 127

Slide 127 text

Peripheral - Open GATT Server override fun onDescriptorWriteRequest( device: BluetoothDevice, requestId: Int, descriptor: BluetoothGattDescriptor, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { super.onDescriptorWriteRequest( device, requestId, descriptor, preparedWrite, responseNeeded, offset, value ) if (UUID_DESCRIPTOR == descriptor.uuid) { if (responseNeeded) { bluetoothGattServer.sendResponse( device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value ) } } }

Slide 128

Slide 128 text

Peripheral - Open GATT Server override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { super.onCharacteristicWriteRequest( device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) if (UUID_CHARACTERISTIC == characteristic.uuid) { try { ... } finally { if (responseNeeded) { bluetoothGattServer.sendResponse( device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value) } } } }

Slide 129

Slide 129 text

Peripheral - Open GATT Server override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { super.onCharacteristicWriteRequest( device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) if (UUID_CHARACTERISTIC == characteristic.uuid) { try { ... } finally { if (responseNeeded) { bluetoothGattServer.sendResponse( device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value) } } } }

Slide 130

Slide 130 text

Peripheral - Open GATT Server if (UUID_CHARACTERISTIC == characteristic.uuid) { try { val setValueResult = characteristic.setValue(value) if (!setValueResult) { return } val message = characteristic.getStringValue(0) } finally { ... } }

Slide 131

Slide 131 text

Peripheral - Open GATT Server if (UUID_CHARACTERISTIC == characteristic.uuid) { try { val setValueResult = characteristic.setValue(value) if (!setValueResult) { return } val message = characteristic.getStringValue(0) } finally { ... } }

Slide 132

Slide 132 text

Peripheral - Start advertising val settingsBuilder = AdvertiseSettings.Builder().apply { setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) setTimeout(TIMEOUT) setConnectable(true) } val dataBuilder = AdvertiseData.Builder().apply { setIncludeTxPowerLevel(true) addServiceUuid(ParcelUuid.fromString(UUID_SERVICE.toString())) } val responseBuilder = AdvertiseData.Builder().apply { setIncludeDeviceName(true) }

Slide 133

Slide 133 text

Peripheral - Start advertising val bluetoothLeAdvertiser: BluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser ?: // This device does not support BLE Peripheral mode. return bluetoothLeAdvertiser.startAdvertising( settingsBuilder.build(), dataBuilder.build(), responseBuilder.build(), advertiseCallback )

Slide 134

Slide 134 text

Peripheral - Start advertising private val advertiseCallback: AdvertiseCallback = object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { Log.d(TAG, "BLE advertising success: $settingsInEffect") } override fun onStartFailure(errorCode: Int) { Log.e(TAG, "BLE advertising failure. errorCode: $errorCode") } }

Slide 135

Slide 135 text

Two-way communication

Slide 136

Slide 136 text

Two-way communication fun write(message: String) { if (isPeripheral) { // Send message via Notification } else { // Send message by Write operation } }

Slide 137

Slide 137 text

Peripheral - write val setValueResult = bluetoothGattCharacteristic.setValue(message) if (!setValueResult) { return false } val notificationResult = bluetoothGattServer.notifyCharacteristicChanged( remoteDevice, bluetoothGattCharacteristic, true) if (!notificationResult) { return false }

Slide 138

Slide 138 text

Peripheral - write val setValueResult = bluetoothGattCharacteristic.setValue(message) if (!setValueResult) { return false } val notificationResult = bluetoothGattServer.notifyCharacteristicChanged( remoteDevice, bluetoothGattCharacteristic, true) if (!notificationResult) { return false }

Slide 139

Slide 139 text

Central - write val setValueResult = bluetoothGattCharacteristic.setValue(telegramString) if (!setValueResult) { return } val writeCharacteristicResult = bluetoothGatt.writeCharacteristic(bluetoothGattCharacteristic) if (!writeCharacteristicResult) { return }

Slide 140

Slide 140 text

Central - write val setValueResult = bluetoothGattCharacteristic.setValue(telegramString) if (!setValueResult) { return } val writeCharacteristicResult = bluetoothGatt.writeCharacteristic(bluetoothGattCharacteristic) if (!writeCharacteristicResult) { return }

Slide 141

Slide 141 text

Tips • The content explained so far is the most basic scenario, and I intentionally dropped important concept, MTU • Requests for larger MTU can be made easily, But there is no guarantee that the request will be accepted • How to handle messages larger than MTU • Central approach • Peripheral approach

Slide 142

Slide 142 text

Request for larger MTU

Slide 143

Slide 143 text

Request for larger MTU private const val MAX_MTU = 512 val requestMtuResult = bluetoothGatt.requestMtu(MAX_MTU) override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { super.onMtuChanged(gatt, mtu, status) currentMTU = mtu }

Slide 144

Slide 144 text

Handle messages larger than MTU - Central

Slide 145

Slide 145 text

Handle messages larger than MTU - Central • Write from Central to Peripheral is relatively straight forward bluetoothGatt.writeCharacteristic(bluetoothGattCharacteristic) • If you try to send a message with more than MTU, the Android framework will automatically try to split the message • In other words, as long as you know how to do it right, this is not dif fi cult to accomplish • This is handled by BluetoothGattServerCallback on Peripheral side

Slide 146

Slide 146 text

Handle messages larger than MTU - Central private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { ... if (UUID_CHARACTERISTIC == characteristic.uuid) { try { if (!preparedWrite) { // keep the same logic as before } else { try { byteArrayOutputStream.write(value) } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 147

Slide 147 text

Handle messages larger than MTU - Central private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { ... if (UUID_CHARACTERISTIC == characteristic.uuid) { try { if (!preparedWrite) { // keep the same logic as before } else { try { byteArrayOutputStream.write(value) } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 148

Slide 148 text

Handle messages larger than MTU - Central private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { ... if (UUID_CHARACTERISTIC == characteristic.uuid) { try { if (!preparedWrite) { // keep the same logic as before } else { try { byteArrayOutputStream.write(value) } catch (e: IOException) { Log.e(TAG, e.message, e) } }

Slide 149

Slide 149 text

Handle messages larger than MTU - Central // The Callback called after all preparedWrites are finished override fun onExecuteWrite(device: BluetoothDevice, requestId: Int, execute: Boolean) { super.onExecuteWrite(device, requestId, execute) val bytes = byteArrayOutputStream.toByteArray() byteArrayOutputStream.reset() val message = String(bytes, StandardCharsets.UTF_8) ... }

Slide 150

Slide 150 text

Handle messages larger than MTU - Peripheral

Slide 151

Slide 151 text

Handle messages larger than MTU - Peripheral • We are using Noti fi cation to send messages from Peripheral, but Noti fi cation cannot send messages larger than MTU • Noti fi cation does not automatically split messages • We need to split and send messages on our own • The minimum MTU seems to be about 20-23 bytes, but write code so that it works even if it changes dynamically depending on the situation

Slide 152

Slide 152 text

Handle messages larger than MTU - Peripheral • Let's de fi ne a protocol • The fi rst byte is the total number of chunks • The next byte is the current index of the total chunk • The remaining bytes are the payload

Slide 153

Slide 153 text

Handle messages larger than MTU - Peripheral private const val MIN_MTU = 20 // This will be overridden later when the exact value is known private var currentMTU = MIN_MTU override fun onMtuChanged(device: BluetoothDevice, mtu: Int) { super.onMtuChanged(device, mtu) currentMTU = mtu }

Slide 154

Slide 154 text

Handle messages larger than MTU - Peripheral private fun sendNotification(message: String): Boolean { val bytes = message.toByteArray(StandardCharsets.UTF_8) val byteSize = bytes.size val headerSize = 2 val payloadSize = currentMTU - headerSize val chunks = (byteSize + payloadSize - 1) / payloadSize if (chunks > 1 shl 8) { return false } ...

Slide 155

Slide 155 text

Handle messages larger than MTU - Peripheral for (i in 0 until chunks) { val srcPos = i * payloadSize val length = if (srcPos + payloadSize > byteSize) byteSize - srcPos else payloadSize val totalChunkByte = chunks.toByte() val currentChunkByte = (i + 1).toByte() val partialBytes = ByteArray(headerSize + length) partialBytes[0] = totalChunkByte partialBytes[1] = currentChunkByte System.arraycopy(bytes, srcPos, partialBytes, headerSize, length) val setValueResult = bluetoothGattCharacteristic.setValue(partialBytes) if (!setValueResult) { return false } val notificationResult = bluetoothGattServer.notifyCharacteristicChanged( remoteDevice, bluetoothGattCharacteristic, true) return notificationResult

Slide 156

Slide 156 text

Handle messages larger than MTU - Peripheral for (i in 0 until chunks) { val srcPos = i * payloadSize val length = if (srcPos + payloadSize > byteSize) byteSize - srcPos else payloadSize val totalChunkByte = chunks.toByte() val currentChunkByte = (i + 1).toByte() val partialBytes = ByteArray(headerSize + length) partialBytes[0] = totalChunkByte partialBytes[1] = currentChunkByte System.arraycopy(bytes, srcPos, partialBytes, headerSize, length) val setValueResult = bluetoothGattCharacteristic.setValue(partialBytes) if (!setValueResult) { return false } val notificationResult = bluetoothGattServer.notifyCharacteristicChanged( remoteDevice, bluetoothGattCharacteristic, true) return notificationResult

Slide 157

Slide 157 text

Handle messages larger than MTU - Peripheral // Central override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { ... } }

Slide 158

Slide 158 text

Handle messages larger than MTU - Peripheral private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { val bytes = characteristic.value val totalChunkByte = bytes[0] val currentChunkByte = bytes[1] val headerSize = 2 val byteSize = bytes.size val buffer = ByteArray(byteSize - headerSize) System.arraycopy(bytes, headerSize, buffer, 0, byteSize - headerSize) val totalChunk = java.lang.Byte.toUnsignedInt(totalChunkByte) val currentChunk = java.lang.Byte.toUnsignedInt(currentChunkByte) try { byteArrayOutputStream.write(buffer) } catch (e: IOException) { Log.e(TAG, e.message, e) } if (totalChunk == currentChunk) { val fullMessage = byteArrayOutputStream.toString() fullMessage.length byteArrayOutputStream.reset() } } }

Slide 159

Slide 159 text

Handle messages larger than MTU - Peripheral private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { val bytes = characteristic.value val totalChunkByte = bytes[0] val currentChunkByte = bytes[1] val headerSize = 2 val byteSize = bytes.size val buffer = ByteArray(byteSize - headerSize) System.arraycopy(bytes, headerSize, buffer, 0, byteSize - headerSize) val totalChunk = java.lang.Byte.toUnsignedInt(totalChunkByte) val currentChunk = java.lang.Byte.toUnsignedInt(currentChunkByte) try { byteArrayOutputStream.write(buffer) } catch (e: IOException) { Log.e(TAG, e.message, e) } if (totalChunk == currentChunk) { val fullMessage = byteArrayOutputStream.toString() fullMessage.length byteArrayOutputStream.reset() } } }

Slide 160

Slide 160 text

Handle messages larger than MTU - Peripheral private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { val bytes = characteristic.value val totalChunkByte = bytes[0] val currentChunkByte = bytes[1] val headerSize = 2 val byteSize = bytes.size val buffer = ByteArray(byteSize - headerSize) System.arraycopy(bytes, headerSize, buffer, 0, byteSize - headerSize) val totalChunk = java.lang.Byte.toUnsignedInt(totalChunkByte) val currentChunk = java.lang.Byte.toUnsignedInt(currentChunkByte) try { byteArrayOutputStream.write(buffer) } catch (e: IOException) { Log.e(TAG, e.message, e) } if (totalChunk == currentChunk) { val fullMessage = byteArrayOutputStream.toString() fullMessage.length byteArrayOutputStream.reset() } } }

Slide 161

Slide 161 text

Handle messages larger than MTU - Peripheral private val byteArrayOutputStream = ByteArrayOutputStream() override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { super.onCharacteristicChanged(gatt, characteristic) if (UUID_CHARACTERISTIC == characteristic.uuid) { val bytes = characteristic.value val totalChunkByte = bytes[0] val currentChunkByte = bytes[1] val headerSize = 2 val byteSize = bytes.size val buffer = ByteArray(byteSize - headerSize) System.arraycopy(bytes, headerSize, buffer, 0, byteSize - headerSize) val totalChunk = java.lang.Byte.toUnsignedInt(totalChunkByte) val currentChunk = java.lang.Byte.toUnsignedInt(currentChunkByte) try { byteArrayOutputStream.write(buffer) } catch (e: IOException) { Log.e(TAG, e.message, e) } if (totalChunk == currentChunk) { val fullMessage = byteArrayOutputStream.toString() fullMessage.length byteArrayOutputStream.reset() } } }

Slide 162

Slide 162 text

Tips • The most important thing with BLE is to ensure that all operations are sequential • Read the of fi cial site documentation properly and make sure you return the response you need • Avoiding the infamous GATT_ERROR(133) is not a matter of needless retries or unfounded SLEEP!

Slide 163

Slide 163 text

How to use each one differently

Slide 164

Slide 164 text

How to use each one di ff erently • When should Wi-Fi Direct be used? • When should Bluetooth Classic be used? • When should BLE be used?

Slide 165

Slide 165 text

When to use Wi-Fi Direct Q. When should Wi-Fi Direct be used? A. When sending and receiving large les

Slide 166

Slide 166 text

When to use Wi-Fi Direct 8J'J %JSFDU #MVFUPPUI $MBTTJD 5SBOTGFS .# fi MF .# 4FD ,# 4FD • TL;DR Wi-Fi is incomparably faster! • Between two Pixel 3a XL • Distance between the two units is 50cm • Average of 50 A to B and 50 B to A runs each (total 100 runs)

Slide 167

Slide 167 text

When to use Wi-Fi Direct • Another set of data shows that Wi-Fi has a high packet reachability rate over long distances • Between Pixel 3a XL and BLU90 • Distance between the two units is 1, 5, 10, 20, and 30m • The graph shows the success rate of packet transmission at each distance

Slide 168

Slide 168 text

When to use Bluetooth Classic Q. When should Bluetooth Classic be used? A. Situations other than sending large les

Slide 169

Slide 169 text

When to use Bluetooth Classic • Studies show that Bluetooth frequency hopping is resistant to network congestion • Decent pairing speed. Wi-Fi Direct takes a few seconds to a dozen seconds before they can connect to each other, while Bluetooth Classic takes 1-2 seconds or less

Slide 170

Slide 170 text

When to use Bluetooth Classic • In my experiments, Bluetooth Classic had the fastest packet arrival time over short to medium distances • Between Pixel 3a XL and BLU90 • Distance between the two units is 1, 5, 10, 20, and 30m • The graph shows the TTL of packet arrival at each distance. The lower the line is located, the faster it is

Slide 171

Slide 171 text

When to use Bluetooth Low Energy Q. When should BLE be used? A. 🤷 I'd love to hear your idea

Slide 172

Slide 172 text

Steps to more advanced topics

Slide 173

Slide 173 text

Nearby Connections API • https://developers.google.com/nearby/connections/overview • ❝Nearby Connections is a peer-to-peer networking API that allows apps to easily discover, connect to, and exchange data with nearby devices in real-time, regardless of network connectivity❞ • It supports multiple network topologies such as peer-to-peer, star, and mesh, depending on the purpose • Looks like the kind of wireless communication we talked about in today's talk will be used under the hood.

Slide 174

Slide 174 text

Any Questions?

Slide 175

Slide 175 text

Thanks!