Nearby Connections for Android: Getting Started
Learn how to exchange data between two Android devices in an offline peer-to-peer fashion using the Nearby Connections API By Fernando Sproviero.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Nearby Connections for Android: Getting Started
20 mins
Discovering
The Discoverer is the device that wants to discover an Advertiser to request a connection.
To start discovering, update the following method:
fun startDiscovering() {
Log.d(TAG, "Start discovering...")
TicTacToeRouter.navigateTo(Screen.Discovering)
val discoveryOptions = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
// 1
connectionsClient.startDiscovery(
BuildConfig.APPLICATION_ID, // 2
endpointDiscoveryCallback, // 3
discoveryOptions // 4
).addOnSuccessListener {
// 5
Log.d(TAG, "Discovering...")
localPlayer = 2
opponentPlayer = 1
}.addOnFailureListener {
// 6
Log.d(TAG, "Unable to start discovering")
TicTacToeRouter.navigateTo(Screen.Home)
}
}
This is what’s going on:
- You call
startDiscovery()
on the client. - You set
BuildConfig.APPLICATION_ID
for service ID because you want to find an Advertiser this unique ID. - Calls to the
endpointDiscoveryCallback
methods occur when establishing a connection with an Advertiser. - You pass the options containing the strategy previously configured.
- Once the client successfully starts discovering you set the local player as player 2, the opponent will be player 1.
- If the client fails to discover, it logs to the console and returns to the home screen.
Add this import:
import com.google.android.gms.nearby.connection.DiscoveryOptions
Add a property named endpointDiscoveryCallback
with the following content:
private val endpointDiscoveryCallback = object : EndpointDiscoveryCallback() {
override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
Log.d(TAG, "onEndpointFound")
}
override fun onEndpointLost(endpointId: String) {
Log.d(TAG, "onEndpointLost")
}
}
You also need to import these:
import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback
import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo
When a Discoverer finds an Advertiser, the Discoverer’s EndpointDiscoveryCallback.onEndpointFound()
will be called. You’ll add code to this method callback in the following section.
Establishing a Connection
After finding an Advertiser, the Discoverer has to request a connection. Update EndpointDiscoveryCallback.onEndpointFound()
with the following code:
override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
Log.d(TAG, "onEndpointFound")
Log.d(TAG, "Requesting connection...")
// 1
connectionsClient.requestConnection(
localUsername, // 2
endpointId, // 3
connectionLifecycleCallback // 4
).addOnSuccessListener {
// 5
Log.d(TAG, "Successfully requested a connection")
}.addOnFailureListener {
// 6
Log.d(TAG, "Failed to request the connection")
}
}
Let’s review step by step:
- You call
requestConnection()
on the client. - You need to pass a local endpoint name.
- Pass the
endpointId
you’ve just found. - Calls to the
connectionLifecycleCallback
methods occur later when the connection initiates with the Advertiser. - Once the client successfully requests a connection, it logs to the console.
- If the client fails, it logs to the console.
The Advertiser and Discoverer need to accept the connection, both will get notified via ConnectionLifecycleCallback.onConnectionInitiated()
, so update the code with this:
override fun onConnectionInitiated(endpointId: String, info: ConnectionInfo) {
Log.d(TAG, "onConnectionInitiated")
Log.d(TAG, "Accepting connection...")
connectionsClient.acceptConnection(endpointId, payloadCallback)
}
You need to provide a payloadCallback
, which contains methods that’ll execute later when the devices exchange data. For now, just create a property with the following content:
private val payloadCallback: PayloadCallback = object : PayloadCallback() {
override fun onPayloadReceived(endpointId: String, payload: Payload) {
Log.d(TAG, "onPayloadReceived")
}
override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
Log.d(TAG, "onPayloadTransferUpdate")
}
}
You need to import these:
import com.google.android.gms.nearby.connection.PayloadCallback
import com.google.android.gms.nearby.connection.Payload
import com.google.android.gms.nearby.connection.PayloadTransferUpdate
After accepting, ConnectionLifecycleCallback.onConnectionResult()
notifies each side of the new connection. Update its code to the following:
override fun onConnectionResult(endpointId: String, resolution: ConnectionResolution) {
Log.d(TAG, "onConnectionResult")
when (resolution.status.statusCode) {
ConnectionsStatusCodes.STATUS_OK -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_OK")
opponentEndpointId = endpointId
Log.d(TAG, "opponentEndpointId: $opponentEndpointId")
newGame()
TicTacToeRouter.navigateTo(Screen.Game)
}
...
If the status code is STATUS_OK
, you save the opponentEndpointId
to send payloads later. Now you can navigate to the game screen to start playing!
Build and run the application on two physical devices, click Host on one of them and Discover on the other one. After a few seconds, you should see the game board on each device:
Using a Payload
Sending
You need to send the player position to the other device whenever you make a move. Modify sendPosition()
with the following code:
private fun sendPosition(position: Pair<Int, Int>) {
Log.d(TAG, "Sending [${position.first},${position.second}] to $opponentEndpointId")
connectionsClient.sendPayload(
opponentEndpointId,
position.toPayLoad()
)
}
Here, you’re using the opponentEndpointId
you previously saved to send the position. You need to convert the position, which is a Pair
to a Payload
object. To do that, add the following extension to the end of the file:
fun Pair<Int, Int>.toPayLoad() = Payload.fromBytes("$first,$second".toByteArray(UTF_8))
Import this:
import kotlin.text.Charsets.UTF_8
You’ve now converted the pair into a comma separated string which is converted to a ByteArray
that is finally used to create a Payload
.
Receiving
To receive this payload, update the PayloadCallback.onPayloadReceived()
with this:
override fun onPayloadReceived(endpointId: String, payload: Payload) {
Log.d(TAG, "onPayloadReceived")
// 1
if (payload.type == Payload.Type.BYTES) {
// 2
val position = payload.toPosition()
Log.d(TAG, "Received [${position.first},${position.second}] from $endpointId")
// 3
play(opponentPlayer, position)
}
}
This is what’s going on:
- You check if the payload type is
BYTES
. - You convert back the
Payload
to a positionPair
object. - Instruct the game that the opponent has played this position.
Add the extension to convert a Payload
to a Pair
position to the end of the file:
fun Payload.toPosition(): Pair<Int, Int> {
val positionStr = String(asBytes()!!, UTF_8)
val positionArray = positionStr.split(",")
return positionArray[0].toInt() to positionArray[1].toInt()
}
Build and run the application on two devices and start playing!
Clearing Connections
When the Advertiser and Discoverer have found each other, you should stop advertising and discovering. Add the following code to the ConnectionLifecycleCallback.onConnectionResult()
:
override fun onConnectionResult(endpointId: String, resolution: ConnectionResolution) {
Log.d(TAG, "onConnectionResult")
when (resolution.status.statusCode) {
ConnectionsStatusCodes.STATUS_OK -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_OK")
connectionsClient.stopAdvertising()
connectionsClient.stopDiscovery()
...
You need to disconnect the client whenever one player decides to exit the game. Add the following to ensure the client is stopped whenever the ViewModel is destroyed:
override fun onCleared() {
stopClient()
super.onCleared()
}
Update goToHome()
as follows:
fun goToHome() {
stopClient()
TicTacToeRouter.navigateTo(Screen.Home)
}
Add the code for stopClient()
as follows:
private fun stopClient() {
Log.d(TAG, "Stop advertising, discovering, all endpoints")
connectionsClient.stopAdvertising()
connectionsClient.stopDiscovery()
connectionsClient.stopAllEndpoints()
localPlayer = 0
opponentPlayer = 0
opponentEndpointId = ""
}
Here you’re also calling stopAllEndpoints()
which will ensure the disconnection of the client.
If you want to disconnect from a specific endpoint you can use disconnectFromEndpoint(endpointId)
.
Finally, whenever an Advertiser or Discoverer executes stopAllEndpoints()
(or disconnectFromEndpoint(endpointId)
) the counterpart will be notified via ConnectionLifecycleCallback.onDisconnected()
, so update it as follows:
override fun onDisconnected(endpointId: String) {
Log.d(TAG, "onDisconnected")
goToHome()
}
Build and run the app on both devices. Start a new game and press the back button on any device. You’ll notice that the game ends on both devices and takes you back to the home screen.
Congratulations! You’ve just learned the basics of Android’s Nearby Connections API.