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

Devices may not always be connected to the internet. Despite that, Nearby Connections allows Android devices within close proximity to connect in a peer-to-peer fashion enabling the exchange of data. This allows use cases such as local multiplayer gaming, offline data transfers and controlling an Android TV using a phone or tablet.

Internally, Nearby Connections combines and abstracts features, such as Bluetooth and Wi-Fi, to create an easy-to-use API. Nearby Connections enables/disables these features as needed and restores the device to its previous state once the app isn’t using the API anymore. This allows you to focus on your specific domain without the worry of integrating complex networking code.

In this tutorial, you’ll learn:

  • What Advertisers and Discoverers are.
  • About advertising your phone for Nearby Connections.
  • How to establish a connection between an advertiser and a discoverer.
  • How to send and receive payloads.
Note: This tutorial assumes you have experience developing in Kotlin. If you’re unfamiliar with the language, read our Kotlin for Android tutorial first.

Getting Started

Throughout this tutorial, you’ll work with a TicTacToe game. In one device, a player will host the match; in another, a second player will connect to the host, and the game will start. The game will let each player know whose turn it is.

Use the Download Materials button at the top or bottom of this tutorial to download the starter project.

Although you could run the starter project using an emulator, later in the tutorial, you’ll need physical devices because, currently, Nearby Connections API requires physical devices to work.

Once downloaded, open the starter project in Android Studio 2021.2.1 or newer. Build and run, and you’ll see the following screen:

Home Screen

You’ll see that you can choose to either host a match or discover an existing one. However, it doesn’t actually do either of those things, you’re going to fix that.

Review the project to familiarize yourself with the files:

  • HomeScreen.kt: Let’s you choose to host or discover a game.
  • WaitingScreen.kt: You’ll find the app’s screens after choosing to host or discover.
  • GameScreen.kt: This contains screens related to the game.
  • TicTacToe.kt: Models a TicTacToe game.
  • TicTacToeRouter.kt: This allows you to navigate between screens.
  • TicTacToeViewModel.kt: This orchestrates the interactions between the screens, the game, and later, with the Nearby Connections client.

Setting Up Dependencies and Permissions

To use the Nearby Connections API, you must first add a dependency. Open your app’s build.gradle file and add the following dependency:

implementation 'com.google.android.gms:play-services-nearby:18.3.0'

Sync your project so Android Studio can download the dependency.

Now open your AndroidManifest.xml and add the following permissions:

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

Some of these are dangerous permissions, therefore you’ll need to request user consent. Open MainActivity and assign REQUIRED_PERMISSIONS inside the companion object as follows:

val REQUIRED_PERMISSIONS =
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    arrayOf(
      Manifest.permission.BLUETOOTH_SCAN,
      Manifest.permission.BLUETOOTH_ADVERTISE,
      Manifest.permission.BLUETOOTH_CONNECT,
      Manifest.permission.ACCESS_FINE_LOCATION
    )
  } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
  } else {
    arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
  }

You’ll need these imports:

import android.Manifest
import android.os.Build

The activity already has the code to request these permissions from the user.

Now that you’ve added the needed dependency, you can start using the Nearby Connections client that you’ll look at in the next section.

Getting the Connection Client

To get the client for Nearby Connections, you can simply call:

Nearby.getConnectionsClient(context)

Because you’ll use it inside the ViewModel, open TicTacToeViewModel and update the constructor with the following:

class TicTacToeViewModel(private val connectionsClient: ConnectionsClient)

Next, open TicTacToeViewModelFactory and update it like this:

class TicTacToeViewModelFactory(
  private val connectionsClient: ConnectionsClient
) : ViewModelProvider.Factory {
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(TicTacToeViewModel::class.java)) {
      @Suppress("UNCHECKED_CAST")
      return TicTacToeViewModel(connectionsClient) as T
  ...

For both files, you’ll need to import the following:

import com.google.android.gms.nearby.connection.ConnectionsClient

Finally, open MainActivity and modify the viewModel property like this:

private val viewModel: TicTacToeViewModel by viewModels {
  TicTacToeViewModelFactory(Nearby.getConnectionsClient(applicationContext))
}

Make sure to import the following:

import com.google.android.gms.nearby.Nearby

Now your ViewModel and associated factory classes have the ConnectionsClient instance provided. You’re ready to start using it and establish a connection!

Choosing a Strategy

Now you’ll choose a connection strategy based on how the devices need to connect.

Check the following table to understand the alternatives:

You would use P2P_CLUSTER when a device can both request outgoing connections to other devices and receive incoming connections from other devices. If you need a star-shaped topology where there’s a central hosting device, and the rest will connect to it, you would use P2P_STAR.

In this case, because you’ll connect between two devices, you’ll use P2P_POINT_TO_POINT. Open TicTacToeViewModel and add the following constant:

private companion object {
  ...
  val STRATEGY = Strategy.P2P_POINT_TO_POINT
}

You’ll need to import:

import com.google.android.gms.nearby.connection.Strategy

It’s important to note that both Advertiser and Discoverer, which you’ll learn about later, have to use the same strategy.

To set the strategy, update startHosting() with the following code:

fun startHosting() {
  Log.d(TAG, "Start advertising...")
  TicTacToeRouter.navigateTo(Screen.Hosting)
  val advertisingOptions = AdvertisingOptions.Builder().setStrategy(STRATEGY).build()
}

This begins the advertising code and sets the strategy to P2P type that you defined earlier. You’ll get back an options variable that you’ll use later to set up the advertising connection.

Also, update startDiscovering() with the following:

fun startDiscovering() {
  Log.d(TAG, "Start discovering...")
  TicTacToeRouter.navigateTo(Screen.Discovering)
  val discoveryOptions = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
}

Similar to the advertising code, this sets up the discovery options to use the same P2P strategy.

In the following sections, you’ll learn what Advertisers and Discoverers are and how they exchange data.

Preparing Your Devices

To start exchanging data between two devices, one of them, the Advertiser, has to advertise itself so that the other device, the Discoverer, can request a connection.

Advertising

To start advertising, update startHosting() with the following:

fun startHosting() {
  Log.d(TAG, "Start advertising...")
  TicTacToeRouter.navigateTo(Screen.Hosting)
  val advertisingOptions = AdvertisingOptions.Builder().setStrategy(STRATEGY).build()

  // 1
  connectionsClient.startAdvertising(
    localUsername, // 2
    BuildConfig.APPLICATION_ID, // 3
    connectionLifecycleCallback, // 4
    advertisingOptions // 5
  ).addOnSuccessListener {
    // 6
    Log.d(TAG, "Advertising...")
    localPlayer = 1
    opponentPlayer = 2
  }.addOnFailureListener {
    // 7
    Log.d(TAG, "Unable to start advertising")
    TicTacToeRouter.navigateTo(Screen.Home)
  }
}

Let’s see what’s going on here:

  1. Call startAdvertising() on the client.
  2. You need to pass a local endpoint name.
  3. You set BuildConfig.APPLICATION_ID for service ID because you want a Discoverer to find you with this unique id.
  4. Calls to the connectionLifecycleCallback methods occur when establishing a connection with a Discoverer.
  5. You pass the options containing the strategy previously configured.
  6. Once the client successfully starts advertising, you set the local player as player 1, and the opponent will be player 2.
  7. If the client fails to advertise, it logs to the console and returns to the home screen.

These are the imports you need:

import com.google.android.gms.nearby.connection.AdvertisingOptions
import com.yourcompany.android.tictactoe.BuildConfig

Add a property named connectionLifecycleCallback with the following content:

private val connectionLifecycleCallback = object : ConnectionLifecycleCallback() {
  override fun onConnectionInitiated(endpointId: String, info: ConnectionInfo) {
    Log.d(TAG, "onConnectionInitiated")
  }

  override fun onConnectionResult(endpointId: String, resolution: ConnectionResolution) {
    Log.d(TAG, "onConnectionResult")

    when (resolution.status.statusCode) {
      ConnectionsStatusCodes.STATUS_OK -> {
        Log.d(TAG, "ConnectionsStatusCodes.STATUS_OK")
      }
      ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED -> {
        Log.d(TAG, "ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED")
      }
      ConnectionsStatusCodes.STATUS_ERROR -> {
        Log.d(TAG, "ConnectionsStatusCodes.STATUS_ERROR")
      }
      else -> {
        Log.d(TAG, "Unknown status code ${resolution.status.statusCode}")
      }
    }
  }

  override fun onDisconnected(endpointId: String) {
    Log.d(TAG, "onDisconnected")
  }
}

When a Discoverer requests a connection, the Advertiser’s ConnectionLifecycleCallback.onConnectionInitiated() will fire. In a later section, you’ll add code to this method callback. When there’s a connection change that occurs, the ConnectionLifecycleCallback.onConnectionResult() fires. You’ll handle three specific connection status types: OK, rejected and error. There’s also a catch-all for the any other unknown status code that is returned.

You’ll need the following imports:

import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback
import com.google.android.gms.nearby.connection.ConnectionInfo
import com.google.android.gms.nearby.connection.ConnectionResolution
import com.google.android.gms.nearby.connection.ConnectionsStatusCodes