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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. You call startDiscovery() on the client.
  2. You set BuildConfig.APPLICATION_ID for service ID because you want to find an Advertiser this unique ID.
  3. Calls to the endpointDiscoveryCallback methods occur when establishing a connection with an Advertiser.
  4. You pass the options containing the strategy previously configured.
  5. Once the client successfully starts discovering you set the local player as player 2, the opponent will be player 1.
  6. 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:

  1. You call requestConnection() on the client.
  2. You need to pass a local endpoint name.
  3. Pass the endpointId you’ve just found.
  4. Calls to the connectionLifecycleCallback methods occur later when the connection initiates with the Advertiser.
  5. Once the client successfully requests a connection, it logs to the console.
  6. 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)
}
Note: Here, you’re immediately accepting the connection; however, you could use an authentication mechanism. For example, instead of just accepting, you could pop a dialog showing a token on both sides, each side can accept or reject the connection. More info here.

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.

Note: If you need to send bigger payloads, check the documentation for more types.

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:

  1. You check if the payload type is BYTES.
  2. You convert back the Payload to a position Pair object.
  3. 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!
Gameplay

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.