Nearby Connections for Android: Getting Started
[ad_1]
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.
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:
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:
Strategy | Request N outgoing connections | Receive M incoming connections |
---|---|---|
P2P_CLUSTER | N=MANY | M=MANY |
P2P_STAR | N=1 | M=MANY |
P2P_POINT_TO_POINT | N=1 | M=1 |
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:
- Call
startAdvertising()
on the client. - You need to pass a local endpoint name.
- You set
BuildConfig.APPLICATION_ID
for service ID because you want a Discoverer to find you with this unique id. - Calls to the
connectionLifecycleCallback
methods occur when establishing a connection with a Discoverer. - You pass the options containing the strategy previously configured.
- Once the client successfully starts advertising, you set the local player as player 1, and the opponent will be player 2.
- 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
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.
Where to Go From Here?
You can download the final version of the project using the Download Materials button at the top or bottom of this tutorial.
As a challenge, you can let more players connect to the host and use a bigger board, the TicTacToe model already allows that. Here are a few tips that will help you:
- You’ll need to choose another connection strategy.
- Let the host decide the board size.
- Because the host doesn’t know how many opponents will connect beforehand, you’ll need to set the local and opponent player numbers when the game starts.
- Instead of immediately starting the game whenever an opponent joins, wait for more than one to join and let the host decide when to start.
- You’ll need a new payload to signal to all the opponents that the host has started a new game.
You can find the solution in the materials.
Here are some great references to learn more about the subject:
- You can find the official documentation here.
- Here you’ll find the API reference.
- If you liked this, you might want to build a ‘rock, paper and scissors’ multiplayer game, just follow along this codelab.
- For other Nearby use cases, check this blog series.
Feel free to share your feedback and findings or ask any questions in the comments below or in the forums. We hope you enjoyed this tutorial!
[ad_2]
Source link